{"version":3,"file":"Resizable.cjs","names":[],"sources":["../../../src/components/Resizable/Resizable.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body owns the pointer capture, the min/max\n * clamp and the keyboard resize step; all three write the same size and must agree\n * on the bounds.\n */\nimport { useCallback, useRef, useState } from \"react\";\nimport type { HTMLAttributes, KeyboardEvent, PointerEvent, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Resizable.module.css\";\n\nexport type ResizableDirection = \"horizontal\" | \"vertical\";\n\nexport interface ResizableProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n    /** Split orientation. `horizontal` (default) places panes side by side. */\n    direction?: ResizableDirection;\n    /** Initial size of the first pane, as a percentage. Defaults to `50`. */\n    defaultSize?: number;\n    /** Lower clamp for the first pane size, in percent. Defaults to `10`. */\n    min?: number;\n    /** Upper clamp for the first pane size, in percent. Defaults to `90`. */\n    max?: number;\n    /** Exactly two panes — `[paneA, paneB]`. */\n    children: [ReactNode, ReactNode];\n}\n\nconst KEY_STEP = 2;\n\n/**\n * Clamp a number into the inclusive `[min, max]` range.\n *\n * @param value - The value to clamp.\n * @param min - Lower bound.\n * @param max - Upper bound.\n * @returns The clamped value.\n */\nfunction clamp(value: number, min: number, max: number): number {\n    return Math.min(max, Math.max(min, value));\n}\n\n/**\n * A two-pane split layout with a draggable divider. The first pane is sized via\n * `flex-basis` as a percentage; the second pane fills the rest. Drag the divider\n * with a pointer, or focus it and use the arrow keys to adjust by 2% steps.\n *\n * The size is clamped to `[min, max]` at all times.\n */\nexport function Resizable({\n    direction = \"horizontal\",\n    defaultSize = 50,\n    min = 10,\n    max = 90,\n    children,\n    className,\n    ...props\n}: ResizableProps) {\n    const [size, setSize] = useState<number>(() => clamp(defaultSize, min, max));\n    const containerRef = useRef<HTMLDivElement | null>(null);\n    const [paneA, paneB] = children;\n    const isHorizontal = direction === \"horizontal\";\n\n    const updateFromPointer = useCallback(\n        (clientX: number, clientY: number): void => {\n            const container = containerRef.current;\n            if (!container) return;\n            const rect = container.getBoundingClientRect();\n            const total = isHorizontal ? rect.width : rect.height;\n            if (total <= 0) return;\n            const offset = isHorizontal ? clientX - rect.left : clientY - rect.top;\n            const percent = (offset / total) * 100;\n            setSize(clamp(percent, min, max));\n        },\n        [isHorizontal, min, max],\n    );\n\n    const handlePointerDown = useCallback(\n        (event: PointerEvent<HTMLDivElement>): void => {\n            event.preventDefault();\n            const divider = event.currentTarget;\n            divider.setPointerCapture(event.pointerId);\n\n            const handleMove = (moveEvent: globalThis.PointerEvent): void => {\n                updateFromPointer(moveEvent.clientX, moveEvent.clientY);\n            };\n            const handleUp = (): void => {\n                window.removeEventListener(\"pointermove\", handleMove);\n                window.removeEventListener(\"pointerup\", handleUp);\n            };\n\n            window.addEventListener(\"pointermove\", handleMove);\n            window.addEventListener(\"pointerup\", handleUp);\n        },\n        [updateFromPointer],\n    );\n\n    const handleKeyDown = useCallback(\n        (event: KeyboardEvent<HTMLDivElement>): void => {\n            let delta = 0;\n            if (isHorizontal) {\n                if (event.key === \"ArrowRight\") delta = KEY_STEP;\n                else if (event.key === \"ArrowLeft\") delta = -KEY_STEP;\n            } else {\n                if (event.key === \"ArrowDown\") delta = KEY_STEP;\n                else if (event.key === \"ArrowUp\") delta = -KEY_STEP;\n            }\n            if (event.key === \"Home\") {\n                event.preventDefault();\n                setSize(min);\n                return;\n            }\n            if (event.key === \"End\") {\n                event.preventDefault();\n                setSize(max);\n                return;\n            }\n            if (delta === 0) return;\n            event.preventDefault();\n            setSize((current) => clamp(current + delta, min, max));\n        },\n        [isHorizontal, min, max],\n    );\n\n    return (\n        <div\n            ref={containerRef}\n            className={cn(\n                styles.root,\n                isHorizontal ? styles.horizontal : styles.vertical,\n                className,\n            )}\n            {...props}\n        >\n            <div className={styles.pane} style={{ flexBasis: `${size}%` }}>\n                {paneA}\n            </div>\n            <div\n                role=\"separator\"\n                aria-orientation={isHorizontal ? \"vertical\" : \"horizontal\"}\n                aria-valuenow={Math.round(size)}\n                aria-valuemin={min}\n                aria-valuemax={max}\n                tabIndex={0}\n                className={styles.divider}\n                onPointerDown={handlePointerDown}\n                onKeyDown={handleKeyDown}\n            >\n                <span className={styles.handle} aria-hidden=\"true\" />\n            </div>\n            <div className={styles.pane} style={{ flexBasis: `${100 - size}%` }}>\n                {paneB}\n            </div>\n        </div>\n    );\n}\n"],"mappings":"gIAyBA,IAAM,EAAW,EAUjB,SAAS,EAAM,EAAe,EAAa,EAAqB,CAC5D,OAAO,KAAK,IAAI,EAAK,KAAK,IAAI,EAAK,CAAK,CAAC,CAC7C,CASA,SAAgB,EAAU,CACtB,YAAY,aACZ,cAAc,GACd,MAAM,GACN,MAAM,GACN,WACA,YACA,GAAG,GACY,CACf,GAAM,CAAC,EAAM,IAAA,EAAW,EAAA,SAAA,KAAuB,EAAM,EAAa,EAAK,CAAG,CAAC,EACrE,GAAA,EAAe,EAAA,OAAA,CAA8B,IAAI,EACjD,CAAC,EAAO,GAAS,EACjB,EAAe,IAAc,aAE7B,GAAA,EAAoB,EAAA,YAAA,EACrB,EAAiB,IAA0B,CACxC,IAAM,EAAY,EAAa,QAC/B,GAAI,CAAC,EAAW,OAChB,IAAM,EAAO,EAAU,sBAAsB,EACvC,EAAQ,EAAe,EAAK,MAAQ,EAAK,OAC/C,GAAI,GAAS,EAAG,OAEhB,IAAM,GADS,EAAe,EAAU,EAAK,KAAO,EAAU,EAAK,KACzC,EAAS,IACnC,EAAQ,EAAM,EAAS,EAAK,CAAG,CAAC,CACpC,EACA,CAAC,EAAc,EAAK,CAAG,CAC3B,EAEM,GAAA,EAAoB,EAAA,YAAA,CACrB,GAA8C,CAC3C,EAAM,eAAe,EAErB,EADsB,cACd,kBAAkB,EAAM,SAAS,EAEzC,IAAM,EAAc,GAA6C,CAC7D,EAAkB,EAAU,QAAS,EAAU,OAAO,CAC1D,EACM,MAAuB,CACzB,OAAO,oBAAoB,cAAe,CAAU,EACpD,OAAO,oBAAoB,YAAa,CAAQ,CACpD,EAEA,OAAO,iBAAiB,cAAe,CAAU,EACjD,OAAO,iBAAiB,YAAa,CAAQ,CACjD,EACA,CAAC,CAAiB,CACtB,EAEM,GAAA,EAAgB,EAAA,YAAA,CACjB,GAA+C,CAC5C,IAAI,EAAQ,EAQZ,GAPI,EACI,EAAM,MAAQ,aAAc,EAAQ,EAC/B,EAAM,MAAQ,cAAa,EAAQ,IAExC,EAAM,MAAQ,YAAa,EAAQ,EAC9B,EAAM,MAAQ,YAAW,EAAQ,IAE1C,EAAM,MAAQ,OAAQ,CACtB,EAAM,eAAe,EACrB,EAAQ,CAAG,EACX,MACJ,CACA,GAAI,EAAM,MAAQ,MAAO,CACrB,EAAM,eAAe,EACrB,EAAQ,CAAG,EACX,MACJ,CACI,IAAU,IACd,EAAM,eAAe,EACrB,EAAS,GAAY,EAAM,EAAU,EAAO,EAAK,CAAG,CAAC,EACzD,EACA,CAAC,EAAc,EAAK,CAAG,CAC3B,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,IAAK,EACL,UAAW,EAAA,GACP,EAAA,QAAO,KACP,EAAe,EAAA,QAAO,WAAa,EAAA,QAAO,SAC1C,CACJ,EACA,GAAI,EAPR,SAAA,EASI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAM,MAAO,CAAE,UAAW,GAAG,EAAK,EAAG,EACvD,SAAA,CACA,CAAA,GACL,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,KAAK,YACL,mBAAkB,EAAe,WAAa,aAC9C,gBAAe,KAAK,MAAM,CAAI,EAC9B,gBAAe,EACf,gBAAe,EACf,SAAU,EACV,UAAW,EAAA,QAAO,QAClB,cAAe,EACf,UAAW,EAEX,UAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OAAQ,cAAY,MAAQ,CAAA,CACnD,CAAA,GACL,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAM,MAAO,CAAE,UAAW,GAAG,IAAM,EAAK,EAAG,EAC7D,SAAA,CACA,CAAA,CACJ,GAEb"}