"use client" import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react" import { useIsXs } from "@/hooks/use-xs" import { getDragPointerSnapshot, notifyDragPointerMove, resetDragPointer, } from "@/lib/drag-pointer-store" export type EmailDragPhase = "dragging" | "returning" export type EmailDragState = { ids: string[] sourceFolderId: string hoveredTargetId: string | null hoveredTargetLabel: string | null /** Last committed pointer (begin drag + snapshot when entering `returning`). */ pointerX: number pointerY: number originX: number originY: number phase: EmailDragPhase } type OnDropCallback = (targetId: string, targetLabel: string, ids: string[]) => void type EmailDragContextValue = { state: EmailDragState | null beginDrag: (ids: string[], sourceFolderId: string, pointerX: number, pointerY: number) => void setHoveredTarget: (id: string | null, label: string | null) => void clearHoveredTarget: (id: string) => void completeDrop: (targetId: string, targetLabel: string) => void cancelDrag: () => void registerOnDrop: (cb: OnDropCallback) => () => void } const EmailDragContext = createContext(null) /** Duration of the "return to origin" animation when drag is cancelled. */ const RETURN_ANIMATION_MS = 240 export function EmailDragProvider({ children }: { children: React.ReactNode }) { const isXs = useIsXs() const [state, setState] = useState(null) const stateRef = useRef(null) const onDropRef = useRef(null) useEffect(() => { stateRef.current = state }, [state]) useEffect(() => { if (!isXs) return stateRef.current = null setState(null) resetDragPointer(0, 0) }, [isXs]) const beginDrag = useCallback( (ids: string[], sourceFolderId: string, pointerX: number, pointerY: number) => { if (isXs || ids.length === 0) return const next: EmailDragState = { ids, sourceFolderId, hoveredTargetId: null, hoveredTargetLabel: null, pointerX, pointerY, originX: pointerX, originY: pointerY, phase: "dragging", } stateRef.current = next resetDragPointer(pointerX, pointerY) setState(next) }, [isXs] ) const setHoveredTarget = useCallback((id: string | null, label: string | null) => { setState((prev) => { if (!prev || prev.phase !== "dragging") return prev if (prev.hoveredTargetId === id) return prev return { ...prev, hoveredTargetId: id, hoveredTargetLabel: label } }) }, []) const clearHoveredTarget = useCallback((id: string) => { setState((prev) => { if (!prev) return prev if (prev.hoveredTargetId !== id) return prev return { ...prev, hoveredTargetId: null, hoveredTargetLabel: null } }) }, []) const cancelDrag = useCallback(() => { const cur = stateRef.current if (!cur) return if (cur.phase === "returning") return const p = getDragPointerSnapshot() const next: EmailDragState = { ...cur, phase: "returning", hoveredTargetId: null, hoveredTargetLabel: null, pointerX: p.x, pointerY: p.y, } stateRef.current = next setState(next) }, []) const completeDrop = useCallback((targetId: string, targetLabel: string) => { const cur = stateRef.current if (!cur) return stateRef.current = null setState(null) resetDragPointer(0, 0) if (targetId !== cur.sourceFolderId && onDropRef.current) { const cb = onDropRef.current const ids = cur.ids queueMicrotask(() => cb(targetId, targetLabel, ids)) } }, []) const registerOnDrop = useCallback((cb: OnDropCallback) => { onDropRef.current = cb return () => { if (onDropRef.current === cb) onDropRef.current = null } }, []) // Live pointer during native drag — updates external store only (no React state). useEffect(() => { if (!state || state.phase !== "dragging") return const onDragOver = (e: DragEvent) => { notifyDragPointerMove(e.clientX, e.clientY) } const onDragEnd = () => { const cur = stateRef.current if (!cur) return if (cur.phase === "returning") return const p = getDragPointerSnapshot() const next: EmailDragState = { ...cur, phase: "returning", hoveredTargetId: null, hoveredTargetLabel: null, pointerX: p.x, pointerY: p.y, } stateRef.current = next setState(next) } window.addEventListener("dragover", onDragOver) window.addEventListener("dragend", onDragEnd) return () => { window.removeEventListener("dragover", onDragOver) window.removeEventListener("dragend", onDragEnd) } }, [state]) // Auto-clear after the return animation finishes. useEffect(() => { if (state?.phase !== "returning") return const id = window.setTimeout(() => { stateRef.current = null setState(null) resetDragPointer(0, 0) }, RETURN_ANIMATION_MS + 20) return () => window.clearTimeout(id) }, [state?.phase]) const value = useMemo( () => ({ state, beginDrag, setHoveredTarget, clearHoveredTarget, completeDrop, cancelDrag, registerOnDrop, }), [ state, beginDrag, setHoveredTarget, clearHoveredTarget, completeDrop, cancelDrag, registerOnDrop, ] ) return {children} } export function useEmailDrag() { const ctx = useContext(EmailDragContext) if (!ctx) { throw new Error("useEmailDrag must be used inside ") } return ctx } export const EMAIL_DRAG_RETURN_MS = RETURN_ANIMATION_MS const noopDropHandlers = { onDragEnter: () => {}, onDragOver: () => {}, onDragLeave: () => {}, onDrop: () => {}, } export function useEmailDropTarget(targetId: string, targetLabel: string) { const isXs = useIsXs() const { state, setHoveredTarget, clearHoveredTarget, completeDrop } = useEmailDrag() if (isXs) { return { isOver: false, acceptsDrop: false, isSource: false, dropHandlers: noopDropHandlers, } } const isDragging = state !== null && state.phase === "dragging" const isSource = !!state && state.sourceFolderId === targetId const acceptsDrop = isDragging && !isSource const isOver = !!state && state.hoveredTargetId === targetId const onDragEnter = (e: React.DragEvent) => { if (!acceptsDrop) return e.preventDefault() setHoveredTarget(targetId, targetLabel) } const onDragOver = (e: React.DragEvent) => { if (!acceptsDrop) return e.preventDefault() if (e.dataTransfer) e.dataTransfer.dropEffect = "move" if (!isOver) setHoveredTarget(targetId, targetLabel) } const onDragLeave = (e: React.DragEvent) => { if (!acceptsDrop) return const rt = e.relatedTarget as Node | null if (rt && e.currentTarget instanceof Node && e.currentTarget.contains(rt)) { return } clearHoveredTarget(targetId) } const onDrop = (e: React.DragEvent) => { if (!acceptsDrop) return e.preventDefault() completeDrop(targetId, targetLabel) } return { isOver: acceptsDrop && isOver, acceptsDrop, isSource, dropHandlers: { onDragEnter, onDragOver, onDragLeave, onDrop }, } }