/ src / components / org-chart / use-org-chart-drag.ts
use-org-chart-drag.ts
 1  'use client'
 2  
 3  import { useCallback, useRef, useState } from 'react'
 4  import type { Transform } from './use-org-chart-pan-zoom'
 5  
 6  export interface DragState {
 7    agentId: string
 8    startX: number
 9    startY: number
10    currentX: number
11    currentY: number
12    dropTargetId: string | null
13  }
14  
15  interface UseDragOpts {
16    transform: Transform
17    containerRef: React.RefObject<HTMLDivElement | null>
18    onDrop: (agentId: string, newParentId: string | null, canvasX: number, canvasY: number) => void
19    findDropTarget: (canvasX: number, canvasY: number, draggedId: string) => string | null
20  }
21  
22  export function useOrgChartDrag({ transform, containerRef, onDrop, findDropTarget }: UseDragOpts) {
23    const [dragState, setDragState] = useState<DragState | null>(null)
24    // Refs mirror state so event handlers never see stale closures
25    const dragRef = useRef<DragState | null>(null)
26    const moved = useRef(false)
27    const pointerIdRef = useRef<number | null>(null)
28  
29    const screenToCanvas = useCallback((sx: number, sy: number) => {
30      const el = containerRef.current
31      if (!el) return { x: 0, y: 0 }
32      const rect = el.getBoundingClientRect()
33      return {
34        x: (sx - rect.left - transform.x) / transform.scale,
35        y: (sy - rect.top - transform.y) / transform.scale,
36      }
37    }, [transform, containerRef])
38  
39    const startDrag = useCallback((e: React.PointerEvent, agentId: string) => {
40      if (e.button !== 0 || e.altKey) return
41      e.stopPropagation()
42      moved.current = false
43      pointerIdRef.current = e.pointerId
44      const canvas = screenToCanvas(e.clientX, e.clientY)
45      const state: DragState = {
46        agentId,
47        startX: canvas.x,
48        startY: canvas.y,
49        currentX: canvas.x,
50        currentY: canvas.y,
51        dropTargetId: null,
52      }
53      dragRef.current = state
54      setDragState(state)
55      // Capture on the container so move/up events route here
56      containerRef.current?.setPointerCapture(e.pointerId)
57    }, [screenToCanvas, containerRef])
58  
59    const moveDrag = useCallback((e: React.PointerEvent) => {
60      const d = dragRef.current
61      if (!d) return
62      moved.current = true
63      const canvas = screenToCanvas(e.clientX, e.clientY)
64      const target = findDropTarget(canvas.x, canvas.y, d.agentId)
65      const next: DragState = { ...d, currentX: canvas.x, currentY: canvas.y, dropTargetId: target }
66      dragRef.current = next
67      setDragState(next)
68    }, [screenToCanvas, findDropTarget])
69  
70    const endDrag = useCallback((_evt: React.PointerEvent) => {
71      const d = dragRef.current
72      if (!d) return
73      if (pointerIdRef.current != null) {
74        containerRef.current?.releasePointerCapture(pointerIdRef.current)
75      }
76      pointerIdRef.current = null
77      dragRef.current = null
78      if (moved.current) {
79        onDrop(d.agentId, d.dropTargetId, d.currentX, d.currentY)
80      }
81      setDragState(null)
82    }, [onDrop, containerRef])
83  
84    const isDragging = dragState != null
85  
86    return { dragState, isDragging, startDrag, moveDrag, endDrag }
87  }