import { useState, useCallback, useRef, useLayoutEffect } from 'react'; interface PaneResizeHandlers { onPointerDown: (e: React.PointerEvent) => void; onPointerMove: (e: React.PointerEvent) => void; onPointerUp: () => void; } export function usePaneResize( initialSize: number, minSize: number, maxSize: number, side: 'left' | 'right' | 'top' | 'bottom' ): [number, PaneResizeHandlers, React.RefCallback] { const [size, setSize] = useState(initialSize); const draggingRef = useRef(false); const liveSizeRef = useRef(initialSize); const targetRef = useRef(null); const containerOffsetRef = useRef(0); const containerSizeRef = useRef(0); const rafRef = useRef(null); const isVertical = side === 'top' || side === 'bottom'; const styleProp = isVertical ? 'height' : 'width'; const targetCallbackRef = useCallback( (el: HTMLElement | null) => { targetRef.current = el; if (el) { el.style[styleProp] = `${liveSizeRef.current}px`; } }, [styleProp] ); // Keep DOM in sync when React state commits (e.g. on pointerUp). // This ensures the ref-managed element always reflects the latest size // without relying on React-controlled style props. useLayoutEffect(() => { if (targetRef.current) { targetRef.current.style[styleProp] = `${size}px`; } }, [size, styleProp]); const computeSize = useCallback( (e: React.PointerEvent): number => { if (isVertical) { const total = containerSizeRef.current || window.innerHeight; const resolvedMax = maxSize <= 1 ? total * maxSize : maxSize; const pos = e.clientY - containerOffsetRef.current; return side === 'top' ? Math.min(resolvedMax, Math.max(minSize, pos)) : Math.min(resolvedMax, Math.max(minSize, total - pos)); } else { const resolvedMax = maxSize <= 1 ? window.innerWidth * maxSize : maxSize; return side === 'left' ? Math.min(resolvedMax, Math.max(minSize, e.clientX)) : Math.min(resolvedMax, Math.max(minSize, window.innerWidth - e.clientX)); } }, [side, isVertical, minSize, maxSize] ); const handlePointerDown = useCallback( (e: React.PointerEvent) => { e.preventDefault(); (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); draggingRef.current = true; if (isVertical) { const container = (e.currentTarget as HTMLElement).parentElement; if (container) { const rect = container.getBoundingClientRect(); containerOffsetRef.current = rect.top; containerSizeRef.current = rect.height; } } }, [isVertical] ); const handlePointerMove = useCallback( (e: React.PointerEvent) => { if (!draggingRef.current) return; liveSizeRef.current = computeSize(e); if (targetRef.current) { // Batch DOM updates to once per animation frame — on mobile, pointermove // can fire multiple times per frame, and each direct style.height write // forces a synchronous reflow that desynchronises MapLibre and deck.gl. if (rafRef.current == null) { rafRef.current = requestAnimationFrame(() => { rafRef.current = null; if (targetRef.current) { targetRef.current.style[styleProp] = `${liveSizeRef.current}px`; } }); } } else { setSize(liveSizeRef.current); } }, [computeSize, styleProp] ); const handlePointerUp = useCallback(() => { draggingRef.current = false; if (rafRef.current != null) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } // Apply final size synchronously so the commit is immediate if (targetRef.current) { targetRef.current.style[styleProp] = `${liveSizeRef.current}px`; } setSize(liveSizeRef.current); }, [styleProp]); return [ size, { onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, }, targetCallbackRef, ]; }