import { MouseEventHandler, useEffect, useRef, useState } from 'react'; export type Pan = { translateX: number; translateY: number; }; const INITIAL_PAN = { translateX: 0, translateY: 0, }; export const usePan = (active: boolean) => { const [pan, setPan] = useState(INITIAL_PAN); const [cursor, setCursor] = useState<'grab' | 'grabbing' | 'initial'>( active ? 'grab' : 'initial', ); // Track the exact handler references that were passed to addEventListener so // we can remove them even if the component re-renders or unmounts mid-drag. const attachedRef = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( null, ); useEffect(() => { setCursor(active ? 'grab' : 'initial'); }, [active]); const handleMouseDown: MouseEventHandler = (evt) => { if (!active) return; evt.preventDefault(); setCursor('grabbing'); const handleMouseMove = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); setPan((p) => ({ translateX: p.translateX + e.movementX, translateY: p.translateY + e.movementY, })); }; const handleMouseUp = (e: MouseEvent) => { e.preventDefault(); setCursor('grab'); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); attachedRef.current = null; }; attachedRef.current = { move: handleMouseMove, up: handleMouseUp }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; useEffect(() => { if (!active) setPan(INITIAL_PAN); }, [active]); // Remove listeners if the component unmounts while a drag is in progress. useEffect( () => () => { if (attachedRef.current) { document.removeEventListener('mousemove', attachedRef.current.move); document.removeEventListener('mouseup', attachedRef.current.up); attachedRef.current = null; } }, [], ); return { pan, cursor, onMouseDown: handleMouseDown, }; };