{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-pane",
  "type": "registry:ui",
  "title": "Split pane",
  "description": "Two places with a divider you can move: dragged one to one, it snaps to the default size or shut on the part spring; keys step it on settle.",
  "dependencies": [],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/split-pane/split-pane.tsx",
      "type": "registry:ui",
      "target": "components/metalui/split-pane/split-pane.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\n/* ─────────────────────────────────────────────────────────\n * SPLIT PANE, two places side by side (or stacked) with a divider you can move\n *\n *   rest      an engraved hairline with a small raised grip at its middle (the switch thumb)\n *   hover     the grip lifts; the cursor says it moves\n *   drag      the panes follow the pointer one to one (no spring while held)\n *   let go    near the default size it snaps there on the part spring (a detent); a collapsible pane\n *             dragged past half its minimum snaps shut the same way; below its minimum it returns\n *             to it\n *   keys      the divider is a focusable separator: arrows step 8 on the settle spring, Home and\n *             End go to the limits (Home collapses a collapsible pane), Enter restores the default\n *   twice     a double-click restores the default\n * Reduce Motion: snaps and steps land at once.\n * A place: it divides area between places. It uses the rule and the switch thumb.\n * Slots: SplitPane.Root, SplitPane.Pane.\n * ───────────────────────────────────────────────────────── */\n\nconst ROOT = 'mu-split-pane relative flex h-full w-full min-h-0 min-w-0 data-[orientation=vertical]:flex-col';\nconst FIRST = 'mu-split-pane-first relative flex-none min-w-0 min-h-0 overflow-auto split-pane-size';\nconst SECOND = 'mu-split-pane-second relative flex-1 min-w-0 min-h-0 overflow-auto';\nconst DIVIDER = 'mu-split-pane-divider relative z-1 flex-none grid place-items-center outline-none touch-none split-pane-divider split-pane-line focus-visible:focus-ring';\nconst GRIP = 'mu-split-pane-grip relative rounded-pill recipe-switch-thumb split-pane-grip reduced-motion:transition-none';\n\nexport interface SplitPaneProps {\n  /** Side by side (horizontal, the default) or stacked (vertical). */\n  orientation?: 'horizontal' | 'vertical';\n  /** The first pane's share, in percent. */\n  size?: number;\n  defaultSize?: number;\n  onSizeChange?: (size: number) => void;\n  /** The first pane's smallest and largest share, in percent. */\n  min?: number;\n  max?: number;\n  /** The first pane may be closed by dragging past half its minimum, or with Home. */\n  collapsible?: boolean;\n  /** Names the divider: \"Resize the sidebar\". */\n  label: string;\n  /** Exactly two panes. */\n  children: [React.ReactNode, React.ReactNode];\n  className?: string;\n}\n\nfunction Pane({ children }: { children?: React.ReactNode }) {\n  return <>{children}</>;\n}\n\nfunction Root({ orientation = 'horizontal', size, defaultSize = 30, onSizeChange, min = 15, max = 85, collapsible, label, children, className }: SplitPaneProps) {\n  const [own, setOwn] = React.useState(defaultSize);\n  const current = size ?? own;\n  const [dragging, setDragging] = React.useState(false);\n  const [snapping, setSnapping] = React.useState(false);\n  const root = React.useRef<HTMLDivElement>(null);\n  const firstId = React.useId();\n  const horizontal = orientation === 'horizontal';\n\n  const set = (v: number, snap = false) => {\n    const next = Math.round(Math.min(max, Math.max(0, v)) * 10) / 10;\n    setSnapping(snap);\n    if (size === undefined) setOwn(next);\n    onSizeChange?.(next);\n  };\n  const extent = () => {\n    const r = root.current!.getBoundingClientRect();\n    return horizontal ? r.width : r.height;\n  };\n  const read = (name: string, fallback: number) => {\n    const el = root.current;\n    return el ? parseFloat(getComputedStyle(el).getPropertyValue(`--mu-r-split-pane-self-${name}`)) || fallback : fallback;\n  };\n\n  const settle = (v: number) => {\n    const detent = read('detent', 3);\n    if (collapsible && v < min / 2) return set(0, true);\n    if (v < min) return set(min, true);\n    if (Math.abs(v - defaultSize) <= detent) return set(defaultSize, true);\n    set(v);\n  };\n\n  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.button !== 0) return;\n    e.currentTarget.setPointerCapture(e.pointerId);\n    setDragging(true);\n  };\n  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!dragging) return;\n    const r = root.current!.getBoundingClientRect();\n    const v = horizontal ? ((e.clientX - r.left) / r.width) * 100 : ((e.clientY - r.top) / r.height) * 100;\n    set(Math.min(max, Math.max(collapsible ? 0 : min, v)));\n  };\n  const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!dragging) return;\n    e.currentTarget.releasePointerCapture(e.pointerId);\n    setDragging(false);\n    settle(current);\n  };\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    const step = (read('step', 8) / extent()) * 100;\n    const back = horizontal ? 'ArrowLeft' : 'ArrowUp';\n    const on = horizontal ? 'ArrowRight' : 'ArrowDown';\n    let next: number | null = null;\n    if (e.key === back) next = Math.max(collapsible && current <= min ? 0 : min, current - step);\n    if (e.key === on) next = Math.min(max, Math.max(current <= 0 ? min : current, current + step));\n    if (e.key === 'Home') next = collapsible ? 0 : min;\n    if (e.key === 'End') next = max;\n    if (e.key === 'Enter') { set(defaultSize, true); e.preventDefault(); return; }\n    if (next != null) { e.preventDefault(); set(next); }\n  };\n\n  return (\n    <div ref={root} data-orientation={orientation} data-dragging={dragging ? '' : undefined} data-snapping={snapping ? '' : undefined} className={className ? `${ROOT} ${className}` : ROOT}>\n      <div id={firstId} className={FIRST} style={{ flexBasis: `${current}%` }}>\n        {children[0]}\n      </div>\n      <div\n        role=\"separator\"\n        tabIndex={0}\n        aria-label={label}\n        aria-controls={firstId}\n        aria-orientation={horizontal ? 'vertical' : 'horizontal'}\n        aria-valuenow={Math.round(current)}\n        aria-valuemin={collapsible ? 0 : min}\n        aria-valuemax={max}\n        data-orientation={orientation}\n        data-dragging={dragging ? '' : undefined}\n        className={DIVIDER}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={onPointerUp}\n        onPointerCancel={onPointerUp}\n        onKeyDown={onKeyDown}\n        onDoubleClick={() => set(defaultSize, true)}\n      >\n        <span aria-hidden data-orientation={orientation} className={GRIP} />\n      </div>\n      <div className={SECOND}>{children[1]}</div>\n    </div>\n  );\n}\n\nexport const SplitPane = Object.assign(Root, { Pane, Root });\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/split-pane.md. SwiftUI: MetalSplitPane in the MetalUI Swift package."
}
