import { useCallback, useRef, useState, type PointerEvent } from "react"; import type { GutterInfo } from "./tree"; /** * A draggable gutter at a split boundary. * * `info.box` is where to render the strip (in container fractions 0–1); * `info.parentBox` is the parent split's bounding box, used to convert * pointer position back into a 0–1 ratio. * * The actual draggable hitbox is a few pixels thick (and centered on the * boundary), but we render a thin visible line via CSS pseudo-elements. */ const HITBOX_PX = 8; export default function Gutter({ info, containerRef, onRatioChange, }: { info: GutterInfo; containerRef: React.RefObject; onRatioChange: (splitId: string, ratio: number) => void; }) { const [dragging, setDragging] = useState(false); const draggingRef = useRef(false); const onPointerDown = useCallback((e: PointerEvent) => { (e.target as HTMLElement).setPointerCapture(e.pointerId); setDragging(true); draggingRef.current = true; e.preventDefault(); e.stopPropagation(); }, []); const onPointerMove = useCallback( (e: PointerEvent) => { if (!draggingRef.current || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return; const xFrac = (e.clientX - rect.left) / rect.width; const yFrac = (e.clientY - rect.top) / rect.height; const pb = info.parentBox; const rawRatio = info.orientation === "h" ? (xFrac - pb.left) / pb.width : (yFrac - pb.top) / pb.height; const ratio = Math.max(0.05, Math.min(0.95, rawRatio)); onRatioChange(info.splitId, ratio); }, [containerRef, info, onRatioChange], ); const onPointerUp = useCallback((e: PointerEvent) => { if (!draggingRef.current) return; (e.target as HTMLElement).releasePointerCapture(e.pointerId); draggingRef.current = false; setDragging(false); }, []); const isH = info.orientation === "h"; // Visible 4px line, but the draggable hitbox is wider for grabbability. const halfHit = HITBOX_PX / 2; const style: React.CSSProperties = isH ? { position: "absolute", top: `${info.box.top * 100}%`, left: `calc(${info.box.left * 100}% - ${halfHit}px)`, height: `${info.box.height * 100}%`, width: `${HITBOX_PX}px`, cursor: "col-resize", zIndex: 10, } : { position: "absolute", top: `calc(${info.box.top * 100}% - ${halfHit}px)`, left: `${info.box.left * 100}%`, width: `${info.box.width * 100}%`, height: `${HITBOX_PX}px`, cursor: "row-resize", zIndex: 10, }; return (
); }