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 }