"use client" /** * Lets the shell's rails glide open and closed instead of snapping. * * `react-resizable-panels` stores a layout as `flex-grow` per panel and writes * it inline, so mounting or unmounting a rail lands the new widths in a single * frame. Nothing about that is animatable from the rail's side — the panel * appears already at its final size — but the row's other panels do change an * animatable property, and interpolating theirs is enough: while main's * `flex-grow` travels, the rail's share of the row grows with it, which reads as * the rail sliding open. * * The transition is armed by an attribute that is present from first render, * because a transition can only animate a change to a property that already had * a value. Arming it later from an effect is too late: panels register in a * layout effect that runs before any parent effect, so the widths are already * written by the time a parent could add a class. * * Two situations must disarm it, and this hook owns both: * * - **A separator drag.** The pointer is the animation. Interpolating on top of * it adds 200ms of lag to every pixel and re-lays out the hub on frames the * user did not ask for. * - **A window resize burst.** Every resize event nudges each panel's grow, so * an armed transition turns a window drag into a rubber-band that trails the * frame behind the cursor. * * Interpolating width does cost a layout per frame — measured at roughly 22fps * on a hub carrying a full `HubTable`, against a single layout for a snap. That * is the deliberate trade: the shell reads as connected motion, and the cost is * bounded to the 200ms the rail is moving. * * @see globals.css — the `[data-rail-transition]` rule this arms */ import * as React from "react" /** Matches the sidebar's own width transition, so the shell moves as one. */ const RAIL_TRANSITION_MS = 200 /** Long enough to outlast a resize event stream, short enough to feel instant. */ const RESIZE_SETTLE_MS = 120 const OFF_ATTRIBUTE = "data-rail-transition-off" export type RailLayoutTransitionProps = { elementRef: React.RefObject "data-rail-transition": "armed" } /** Spread the result onto the `ResizablePanelGroup` holding the rails. */ export function useRailLayoutTransition(): RailLayoutTransitionProps { const elementRef = React.useRef(null) React.useEffect(() => { const group = elementRef.current if (!group) return let settleTimer: number | undefined // Written straight to the DOM rather than held in state: this is a hint for // the compositor during a gesture, and a re-render per pointer event is the // opposite of what a gesture needs. const disarm = () => group.setAttribute(OFF_ATTRIBUTE, "") const rearm = () => group.removeAttribute(OFF_ATTRIBUTE) const endDrag = () => { rearm() window.removeEventListener("pointerup", endDrag) window.removeEventListener("pointercancel", endDrag) } const onPointerDown = (event: PointerEvent) => { const target = event.target if (!(target instanceof Element) || !target.closest("[data-separator]")) { return } disarm() window.addEventListener("pointerup", endDrag) window.addEventListener("pointercancel", endDrag) } // Arrow-key resizing is a gesture too, one keypress at a time. const onKeyDown = (event: KeyboardEvent) => { const target = event.target if (!(target instanceof Element) || !target.closest("[data-separator]")) { return } if (!event.key.startsWith("Arrow")) return disarm() window.clearTimeout(settleTimer) settleTimer = window.setTimeout(rearm, RESIZE_SETTLE_MS) } const onWindowResize = () => { disarm() window.clearTimeout(settleTimer) settleTimer = window.setTimeout(rearm, RESIZE_SETTLE_MS) } group.addEventListener("pointerdown", onPointerDown, true) group.addEventListener("keydown", onKeyDown, true) window.addEventListener("resize", onWindowResize, { passive: true }) return () => { group.removeEventListener("pointerdown", onPointerDown, true) group.removeEventListener("keydown", onKeyDown, true) window.removeEventListener("resize", onWindowResize) window.clearTimeout(settleTimer) endDrag() window.clearTimeout(settleTimer) } }, []) return { elementRef, "data-rail-transition": "armed" } } export { RAIL_TRANSITION_MS }