/** * useResizablePanel - Hook for creating resizable panels with persistence * * Features: * - Drag to resize * - Min/max constraints * - localStorage persistence * - Double-click to reset to default */ import { useState, useCallback, useEffect, useRef } from 'react'; export interface ResizablePanelConfig { /** Unique key for localStorage persistence */ storageKey: string; /** Default width in pixels */ defaultWidth: number; /** Minimum width in pixels */ minWidth: number; /** Maximum width in pixels */ maxWidth: number; /** Side of the panel ('left' or 'right') - affects resize direction */ side: 'left' | 'right'; } export interface ResizablePanelState { /** Current width in pixels */ width: number; /** Whether user is currently dragging */ isDragging: boolean; /** Start drag handler - attach to resize handle mousedown */ startDrag: (e: React.MouseEvent) => void; /** Reset to default width */ resetWidth: () => void; /** Set width programmatically */ setWidth: (width: number) => void; } const STORAGE_PREFIX = 'atomizer-panel-'; function getStoredWidth(key: string, defaultWidth: number): number { if (typeof window === 'undefined') return defaultWidth; try { const stored = localStorage.getItem(STORAGE_PREFIX + key); if (stored) { const parsed = parseInt(stored, 10); if (!isNaN(parsed)) return parsed; } } catch { // localStorage not available } return defaultWidth; } function storeWidth(key: string, width: number): void { if (typeof window === 'undefined') return; try { localStorage.setItem(STORAGE_PREFIX + key, String(width)); } catch { // localStorage not available } } export function useResizablePanel(config: ResizablePanelConfig): ResizablePanelState { const { storageKey, defaultWidth, minWidth, maxWidth, side } = config; // Initialize from localStorage const [width, setWidthState] = useState(() => { const stored = getStoredWidth(storageKey, defaultWidth); return Math.max(minWidth, Math.min(maxWidth, stored)); }); const [isDragging, setIsDragging] = useState(false); // Track initial position for drag calculation const dragStartRef = useRef<{ x: number; width: number } | null>(null); // Clamp width within bounds const clampWidth = useCallback((w: number) => { return Math.max(minWidth, Math.min(maxWidth, w)); }, [minWidth, maxWidth]); // Set width with clamping and persistence const setWidth = useCallback((newWidth: number) => { const clamped = clampWidth(newWidth); setWidthState(clamped); storeWidth(storageKey, clamped); }, [clampWidth, storageKey]); // Reset to default const resetWidth = useCallback(() => { setWidth(defaultWidth); }, [defaultWidth, setWidth]); // Start drag handler const startDrag = useCallback((e: React.MouseEvent) => { e.preventDefault(); setIsDragging(true); dragStartRef.current = { x: e.clientX, width }; }, [width]); // Handle mouse move during drag useEffect(() => { if (!isDragging) return; const handleMouseMove = (e: MouseEvent) => { if (!dragStartRef.current) return; const delta = e.clientX - dragStartRef.current.x; // For left panels, positive delta increases width // For right panels, negative delta increases width const newWidth = side === 'left' ? dragStartRef.current.width + delta : dragStartRef.current.width - delta; setWidthState(clampWidth(newWidth)); }; const handleMouseUp = () => { if (dragStartRef.current) { // Persist the final width storeWidth(storageKey, width); } setIsDragging(false); dragStartRef.current = null; }; // Add listeners to document for smooth dragging document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); // Change cursor globally during drag document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; }; }, [isDragging, side, clampWidth, storageKey, width]); return { width, isDragging, startDrag, resetWidth, setWidth, }; } export default useResizablePanel;