{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "resizable",
  "type": "registry:component",
  "title": "Resizable",
  "description": "Resizes two adjacent panes with a draggable 1px handle. Arrow keys adjust the split.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/vlak-lib.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/resizable.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { useMergedRefs } from \"./merge-refs\";\n\nexport interface SplitProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Starting share of the first pane, in percent. */\n  initial?: number;\n  min?: number;\n  max?: number;\n  /** Accessible name of the handle. */\n  handleLabel?: string;\n  children: [React.ReactNode, React.ReactNode];\n}\n\nconst styles = stylex.create({\n  split: {\n    display: \"flex\",\n    flexDirection: {\n      default: \"row\",\n      [mq.phone]: \"column\",\n    },\n    width: \"100%\",\n  },\n  pane: {\n    minWidth: 0,\n    overflow: \"hidden\",\n  },\n  handle: {\n    flexShrink: 0,\n    width: {\n      default: vlak.hit,\n      [mq.phone]: \"auto\",\n    },\n    height: {\n      default: null,\n      [mq.phone]: vlak.hit,\n    },\n    marginTop: {\n      default: 0,\n      [mq.phone]: \"-0.25rem\",\n    },\n    marginBottom: {\n      default: 0,\n      [mq.phone]: \"-0.25rem\",\n    },\n    marginInlineStart: {\n      default: \"-1.34375rem\",\n      [mq.phone]: 0,\n    },\n    marginInlineEnd: {\n      default: \"-1.34375rem\",\n      [mq.phone]: 0,\n    },\n    position: \"relative\",\n    zIndex: vlak.zRaised,\n    cursor: {\n      default: \"col-resize\",\n      [mq.phone]: \"row-resize\",\n    },\n    backgroundColor: \"transparent\",\n    borderWidth: 0,\n    borderStyle: \"none\",\n    padding: 0,\n    touchAction: \"none\",\n    outlineWidth: {\n      default: null,\n      \":focus-visible\": 2,\n    },\n    outlineStyle: {\n      default: null,\n      \":focus-visible\": \"solid\",\n    },\n    outlineColor: {\n      default: null,\n      \":focus-visible\": vlak.ink,\n    },\n    outlineOffset: {\n      default: null,\n      \":focus-visible\": 2,\n    },\n    \"::after\": {\n      content: '\"\"',\n      position: \"absolute\",\n      insetInlineStart: {\n        default: \"1.34375rem\",\n        [mq.phone]: 0,\n      },\n      insetInlineEnd: {\n        default: null,\n        [mq.phone]: 0,\n      },\n      top: {\n        default: 0,\n        [mq.phone]: \"50%\",\n      },\n      bottom: {\n        default: 0,\n        [mq.phone]: \"auto\",\n      },\n      width: {\n        default: 1,\n        [mq.phone]: \"auto\",\n      },\n      height: {\n        default: null,\n        [mq.phone]: 1,\n      },\n      backgroundColor: {\n        default: vlak.controlBorder,\n        [mq.forcedColors]: \"CanvasText\",\n      },\n      forcedColorAdjust: \"none\",\n      transition: {\n        default: `background-color ${vlak.durationSnap} ${vlak.ease}`,\n        [mq.reduce]: \"none\",\n      },\n    },\n    \":hover::after\": {\n      backgroundColor: {\n        default: null,\n        [mq.hover]: {\n          default: vlak.ink,\n          [mq.forcedColors]: \"Highlight\",\n        },\n      },\n    },\n    \":focus-visible::after\": {\n      backgroundColor: {\n        default: vlak.ink,\n        [mq.forcedColors]: \"Highlight\",\n      },\n    },\n  },\n});\n\nconst STACK_QUERY = \"(max-width: 640px)\";\nconst noop = () => {};\n\n/** True once the split stacks (the phone recut). False on the server and where matchMedia is missing. */\nfunction useStacked(): boolean {\n  const subscribe = React.useCallback((onChange: () => void) => {\n    if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return noop;\n    const mql = window.matchMedia(STACK_QUERY);\n    mql.addEventListener(\"change\", onChange);\n    return () => mql.removeEventListener(\"change\", onChange);\n  }, []);\n  const read = () =>\n    typeof window !== \"undefined\" && typeof window.matchMedia === \"function\"\n      ? window.matchMedia(STACK_QUERY).matches\n      : false;\n  return React.useSyncExternalStore(subscribe, read, () => false);\n}\n\n/** Two panes on a draggable hairline. Arrows step, Home/End jump; the axis follows the layout. */\nexport const Split = React.forwardRef<HTMLDivElement, SplitProps>(function Split({\n  initial = 50,\n  min = 20,\n  max = 80,\n  handleLabel = \"Resize panes\",\n  className,\n  style,\n  children,\n  ...props\n}: SplitProps, ref: React.ForwardedRef<HTMLDivElement>) {\n  const [share, setShare] = React.useState(initial);\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const setRootRef = useMergedRefs(rootRef, ref);\n  const stacked = useStacked();\n  const clamp = (v: number) => Math.min(max, Math.max(min, v));\n\n  const onPointerDown = (e: React.PointerEvent<HTMLButtonElement>) => {\n    e.preventDefault();\n    (e.target as HTMLElement).setPointerCapture(e.pointerId);\n    const rect = rootRef.current?.getBoundingClientRect();\n    if (!rect) return;\n    const move = (ev: PointerEvent) =>\n      setShare(\n        clamp(\n          stacked\n            ? ((ev.clientY - rect.top) / rect.height) * 100\n            : ((ev.clientX - rect.left) / rect.width) * 100,\n        ),\n      );\n    const up = () => {\n      document.removeEventListener(\"pointermove\", move);\n      document.removeEventListener(\"pointerup\", up);\n    };\n    document.addEventListener(\"pointermove\", move);\n    document.addEventListener(\"pointerup\", up);\n  };\n\n  const onKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n    const step = e.shiftKey ? 10 : 2;\n    let next: number | null = null;\n    if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") next = share - step;\n    else if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") next = share + step;\n    else if (e.key === \"Home\") next = min;\n    else if (e.key === \"End\") next = max;\n    if (next === null) return;\n    e.preventDefault();\n    setShare(clamp(next));\n  };\n\n  const root = rs([\"rs-split\", className], styles.split);\n  const pane = rs([\"rs-split-pane\"], styles.pane);\n  const handle = rs([\"rs-split-handle\"], styles.handle);\n  const size = (pct: number): React.CSSProperties =>\n    stacked ? { height: `${pct}%`, width: \"auto\" } : { width: `${pct}%` };\n\n  return (\n    <div ref={setRootRef} {...props} className={root.className} style={{ ...root.style, ...style }}>\n      <div className={pane.className} style={{ ...pane.style, ...size(share) }}>\n        {children[0]}\n      </div>\n      <button\n        type=\"button\"\n        className={handle.className}\n        style={handle.style}\n        role=\"separator\"\n        aria-orientation={stacked ? \"horizontal\" : \"vertical\"}\n        aria-valuenow={Math.round(share)}\n        aria-valuemin={min}\n        aria-valuemax={max}\n        aria-label={handleLabel}\n        onPointerDown={onPointerDown}\n        onKeyDown={onKeyDown}\n      />\n      <div className={pane.className} style={{ ...pane.style, ...size(100 - share) }}>\n        {children[1]}\n      </div>\n    </div>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/vlak/resizable.tsx"
    },
    {
      "path": "vlak/styles/resizable.css",
      "content": "/* ── resizable: generated from packages/react/src/components/resizable.tsx ── */\n.rs-split{display:flex;flex-direction:row;width:100%}\n@media (max-width: 640px){.rs-split{flex-direction:column}}\n.rs-split-pane{min-width:0;overflow:hidden}\n.rs-split-handle{flex-shrink:0;width:var(--hit);margin-top:0;margin-bottom:0;margin-inline-start:-1.34375rem;margin-inline-end:-1.34375rem;position:relative;z-index:var(--z-raised);cursor:col-resize;background-color:transparent;border-width:0;border-style:none;padding:0;touch-action:none}\n.rs-split-handle:focus-visible{outline-width:2px;outline-style:solid;outline-color:var(--text);outline-offset:2px}\n.rs-split-handle::after{content:\"\";position:absolute;inset-inline-start:1.34375rem;top:0;bottom:0;width:1px;background-color:var(--control-border);forced-color-adjust:none;transition:background-color var(--duration-snap) var(--ease)}\n.rs-split-handle:focus-visible::after{background-color:var(--text)}\n@media (max-width: 640px){.rs-split-handle{width:auto;height:var(--hit);margin-top:-0.25rem;margin-bottom:-0.25rem;margin-inline-start:0;margin-inline-end:0;cursor:row-resize}.rs-split-handle::after{inset-inline-start:0;inset-inline-end:0;top:50%;bottom:auto;width:auto;height:1px}}\n@media (forced-colors: active){.rs-split-handle::after{background-color:CanvasText}.rs-split-handle:focus-visible::after{background-color:Highlight}}\n@media (prefers-reduced-motion: reduce){.rs-split-handle::after{transition:none}}\n@media (hover: hover) and (pointer: fine){.rs-split-handle:hover::after{background-color:var(--text)}}\n@media (hover: hover) and (pointer: fine) and (forced-colors: active){.rs-split-handle:hover::after{background-color:Highlight}}\n",
      "type": "registry:file",
      "target": "styles/vlak/resizable.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "surfaces",
      "classes": [
        "rs-split",
        "rs-split-pane",
        "rs-split-handle"
      ],
      "snippet": "<div class=\"rs-split\"><div class=\"rs-split-pane\" style=\"width:50%\">Left</div><button class=\"rs-split-handle\" role=\"separator\" aria-valuenow=\"50\"></button><div class=\"rs-split-pane\" style=\"width:50%\">Right</div></div>",
      "cssOnly": false,
      "registryDependencies": [
        "vlak-lib"
      ],
      "aliases": [
        "Resizable",
        "Split pane",
        "Resizable panels",
        "Splitter",
        "Panel group"
      ],
      "example": "import { Split } from \"@noorddev/vlak-react\";\n\n<Split initial={60} min={30} max={80} handleLabel=\"Resize editor and preview\">\n  <Editor />\n  <Preview />\n</Split>",
      "usage": {
        "use": [
          "Two panes the user sizes against each other: editor and preview, list and detail.",
          "Percentages for initial, min, and max."
        ],
        "avoid": [
          "More than two panes; nest a Split.",
          "Layouts that should stack on phones by other rules; it stacks at 640px on its own."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Focuses the handle"
        },
        {
          "keys": "Arrow left, Arrow up",
          "does": "Shrinks the first pane by 2%"
        },
        {
          "keys": "Arrow right, Arrow down",
          "does": "Grows it by 2%"
        },
        {
          "keys": "Shift + Arrow keys",
          "does": "Moves by 10%"
        },
        {
          "keys": "Home, End",
          "does": "Jumps to min or max"
        }
      ],
      "a11y": [
        "The handle is a <button role=\"separator\"> with aria-orientation, aria-valuenow, aria-valuemin, aria-valuemax, and aria-label from handleLabel.",
        "Below 640px the panes stack and the axis follows: the orientation flips and the arrows move vertically."
      ]
    }
  }
}
