import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; type Props = { direction?: "horizontal" | "vertical"; initialRatio?: number; storageKey?: string; minPx?: number; minSecondPx?: number; className?: string; children: [ReactNode, ReactNode]; }; function clamp(n: number, lo: number, hi: number) { return Math.min(hi, Math.max(lo, n)); } export function SplitPane({ direction = "vertical", initialRatio = 0.45, storageKey, minPx = 120, minSecondPx = minPx, className = "", children, }: Props) { const [a, b] = children; const rootRef = useRef(null); const [size, setSize] = useState(0); const [ratio, setRatio] = useState(() => { if (storageKey) { const raw = localStorage.getItem(storageKey); const n = raw ? Number(raw) : NaN; if (Number.isFinite(n) && n > 0.1 && n < 0.9) return n; } return initialRatio; }); const dragging = useRef(false); useEffect(() => { const root = rootRef.current; if (!root) return; const measure = () => setSize(direction === "vertical" ? root.clientHeight : root.clientWidth); const observer = new ResizeObserver(measure); observer.observe(root); measure(); return () => observer.disconnect(); }, [direction]); useEffect(() => { if (storageKey) localStorage.setItem(storageKey, String(ratio)); }, [ratio, storageKey]); const onPointerMove = useCallback( (e: PointerEvent) => { if (!dragging.current || !rootRef.current) return; const rect = rootRef.current.getBoundingClientRect(); if (direction === "vertical") { const y = e.clientY - rect.top; if (rect.height) setRatio(clamp(y / rect.height, minPx / rect.height, 1 - (minSecondPx + 8) / rect.height)); } else { const x = e.clientX - rect.left; if (rect.width) setRatio(clamp(x / rect.width, minPx / rect.width, 1 - (minSecondPx + 8) / rect.width)); } }, [direction, minPx, minSecondPx], ); const stop = useCallback(() => { dragging.current = false; document.body.style.cursor = ""; document.body.style.userSelect = ""; }, []); useEffect(() => { window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointerup", stop); return () => { window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointerup", stop); }; }, [onPointerMove, stop]); const vert = direction === "vertical"; // A stored ratio can become unusable after shrinking the window. Reserve room // for both panes and the separator at the current measured size. const firstSize = size ? Math.min(Math.max(size * ratio, Math.min(minPx, size / 2)), Math.max(0, size - minSecondPx - 8)) : undefined; return (
{a}
{ e.preventDefault(); dragging.current = true; document.body.style.cursor = vert ? "row-resize" : "col-resize"; document.body.style.userSelect = "none"; }} onDoubleClick={() => setRatio(initialRatio)} className={ vert ? "group relative z-10 h-2 shrink-0 cursor-row-resize" : "group relative z-10 w-2 shrink-0 cursor-col-resize" } style={{ background: "transparent" }} >
{b}
); }