// A drag-to-resize width for a side panel (the Study stage's right column). The handle owns // pointer dragging, arrow keys and double-click reset; the width is remembered per device. import { useCallback, useRef, useState } from 'react'; const STEP = 24; const BIG_STEP = 96; // containerRef: the element the panel sits at the right edge of (for measuring and clamping). export function useResizableWidth({ storageKey, min = 320, maxAbsolute = 760, fallback = 448, reserve = 600, containerRef }) { const maxFor = () => { const w = containerRef.current?.getBoundingClientRect().width ?? 0; return w > 0 ? Math.max(min, Math.min(maxAbsolute, w - reserve)) : maxAbsolute; }; const clamp = (n) => Math.round(Math.min(maxFor(), Math.max(min, n))); const [width, setWidthState] = useState(() => { try { const n = Number(localStorage.getItem(storageKey)); return Number.isFinite(n) && n > 0 ? Math.min(maxAbsolute, Math.max(min, Math.round(n))) : null; } catch { return null; } }); const dragRef = useRef(null); const commit = useCallback((n) => { setWidthState(n); try { if (n == null) localStorage.removeItem(storageKey); else localStorage.setItem(storageKey, String(n)); } catch { /* storage unavailable */ } }, [storageKey]); // Width of the panel right now (state, else measured from the panel element, else a default). const current = (panelEl) => width ?? (panelEl?.getBoundingClientRect().width || fallback); const handleProps = (panelRef) => ({ role: 'separator', 'aria-orientation': 'vertical', 'aria-label': 'Resize side panel', 'aria-valuemin': min, 'aria-valuemax': maxAbsolute, 'aria-valuenow': Math.round(current(panelRef.current)), tabIndex: 0, onPointerDown: (e) => { if (e.button !== undefined && e.button !== 0) return; e.currentTarget.setPointerCapture?.(e.pointerId); dragRef.current = { id: e.pointerId }; document.body.style.userSelect = 'none'; }, onPointerMove: (e) => { if (!dragRef.current) return; const right = containerRef.current?.getBoundingClientRect().right ?? window.innerWidth; const next = clamp(right - e.clientX); dragRef.current.width = next; containerRef.current?.style.setProperty('--study-right', `${next}px`); // smooth: no React render per move }, onPointerUp: (e) => { if (!dragRef.current) return; e.currentTarget.releasePointerCapture?.(e.pointerId); const done = dragRef.current.width; dragRef.current = null; document.body.style.userSelect = ''; if (done != null) commit(done); }, onKeyDown: (e) => { const step = e.shiftKey ? BIG_STEP : STEP; if (e.key === 'ArrowLeft') { e.preventDefault(); commit(clamp(current(panelRef.current) + step)); } else if (e.key === 'ArrowRight') { e.preventDefault(); commit(clamp(current(panelRef.current) - step)); } else if (e.key === 'Home' || e.key === 'Escape') { e.preventDefault(); containerRef.current?.style.removeProperty('--study-right'); commit(null); } }, onDoubleClick: () => { containerRef.current?.style.removeProperty('--study-right'); commit(null); }, title: 'Drag to resize ยท double-click to reset', }); return { width, handleProps }; }