{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-pane",
  "title": "Split Pane",
  "description": "Two resizable regions with keyboard controls and saved proportions.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/split-pane.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  type ComponentProps,\n  createContext,\n  type KeyboardEvent,\n  type PointerEvent,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const SPLIT_PANE_VARIANTS = [\"card\", \"flush\", \"inset\"] as const;\nexport type SplitPaneVariant = (typeof SPLIT_PANE_VARIANTS)[number];\nexport type SplitPaneOrientation = \"horizontal\" | \"vertical\";\nexport type SplitPaneProps = Omit<ComponentProps<\"div\">, \"defaultValue\"> & {\n  variant?: SplitPaneVariant;\n  orientation?: SplitPaneOrientation;\n  /** Size of the primary region as a percentage of the container. */\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (value: number) => void;\n  min?: number;\n  max?: number;\n  step?: number;\n  /** Persist the proportion in localStorage under this key. */\n  storageKey?: string;\n};\ntype SplitContext = {\n  id: string;\n  value: number;\n  min: number;\n  max: number;\n  step: number;\n  orientation: SplitPaneOrientation;\n  variant: SplitPaneVariant;\n  rootRef: React.RefObject<HTMLDivElement | null>;\n  change: (value: number) => void;\n};\nconst Context = createContext<SplitContext | null>(null);\nfunction useSplit(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within SplitPane`);\n  return context;\n}\nconst splitPaneVariants = cva(\n  \"flex min-h-0 min-w-0 overflow-hidden text-[13px]/[18px] text-foreground\",\n  {\n    variants: {\n      variant: {\n        card: \"gap-0 rounded-[14px] border bg-card p-0\",\n        flush: \"gap-0 rounded-none border-0 bg-card p-0\",\n        inset: \"gap-1 rounded-[14px] border-0 bg-muted p-1\",\n      },\n      orientation: { horizontal: \"flex-row\", vertical: \"flex-col\" },\n    },\n  },\n);\nconst splitPanePaneVariants = cva(\"min-h-0 min-w-0 overflow-auto bg-card\", {\n  variants: {\n    variant: {\n      card: \"rounded-none\",\n      flush: \"rounded-none\",\n      inset: \"rounded-[10px] shadow-[0_1px_2px_oklch(0_0_0/0.06)]\",\n    },\n  },\n});\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, Math.round(value * 10) / 10));\n}\n\nexport function SplitPane({\n  variant = \"card\",\n  orientation = \"horizontal\",\n  value,\n  defaultValue = 50,\n  onValueChange,\n  min = 20,\n  max = 80,\n  step = 5,\n  storageKey,\n  children,\n  className,\n  ...props\n}: SplitPaneProps) {\n  const id = useId();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [internal, setInternal] = useState(() => clamp(defaultValue, min, max));\n  const current = clamp(value ?? internal, min, max);\n  const change = (next: number) => {\n    const bounded = clamp(next, min, max);\n    if (bounded === current) return;\n    if (value === undefined) setInternal(bounded);\n    onValueChange?.(bounded);\n    if (storageKey) {\n      try {\n        window.localStorage.setItem(storageKey, String(bounded));\n      } catch {}\n    }\n  };\n  const restore = useRef({ storageKey, min, max, value, onValueChange });\n  useEffect(() => {\n    const {\n      storageKey: key,\n      min: low,\n      max: high,\n      value: controlled,\n      onValueChange: notify,\n    } = restore.current;\n    if (!key) return;\n    let stored: number;\n    try {\n      stored = Number(window.localStorage.getItem(key));\n    } catch {\n      return;\n    }\n    if (!Number.isFinite(stored) || stored <= 0) return;\n    const bounded = clamp(stored, low, high);\n    if (controlled === undefined) setInternal(bounded);\n    notify?.(bounded);\n  }, []);\n  return (\n    <Context.Provider\n      value={{ id, value: current, min, max, step, orientation, variant, rootRef, change }}\n    >\n      <div\n        data-slot=\"split-pane\"\n        className={cn(splitPaneVariants({ variant, orientation }), className)}\n        {...props}\n        ref={rootRef}\n        data-variant={variant}\n        data-orientation={orientation}\n      >\n        {children}\n      </div>\n    </Context.Provider>\n  );\n}\n\nexport function SplitPanePrimary({ className, style, ...props }: ComponentProps<\"div\">) {\n  const context = useSplit(\"SplitPanePrimary\");\n  return (\n    <div\n      data-slot=\"split-pane-primary\"\n      className={cn(splitPanePaneVariants({ variant: context.variant }), className)}\n      {...props}\n      id={`${context.id}-primary`}\n      data-split-pane-region=\"primary\"\n      style={{ flex: `0 0 ${context.value}%`, ...style }}\n    />\n  );\n}\n\nexport function SplitPaneSecondary({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useSplit(\"SplitPaneSecondary\");\n  return (\n    <div\n      data-slot=\"split-pane-secondary\"\n      className={cn(splitPanePaneVariants({ variant: context.variant }), \"flex-[1_1_0]\", className)}\n      {...props}\n      data-split-pane-region=\"secondary\"\n    />\n  );\n}\n\nexport function SplitPaneHandle({\n  onKeyDown,\n  onPointerDown,\n  onPointerMove,\n  onPointerUp,\n  className,\n  ...props\n}: ComponentProps<\"div\">) {\n  const context = useSplit(\"SplitPaneHandle\");\n  const [dragging, setDragging] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const lastExpanded = useRef(context.value);\n  const horizontal = context.orientation === \"horizontal\";\n  const toPercent = (event: PointerEvent<HTMLDivElement>) => {\n    const box = context.rootRef.current?.getBoundingClientRect();\n    if (!box) return context.value;\n    const size = horizontal ? box.width : box.height;\n    if (!size) return context.value;\n    const offset = horizontal ? event.clientX - box.left : event.clientY - box.top;\n    return (offset / size) * 100;\n  };\n  const keys: Record<string, () => number> = {\n    [horizontal ? \"ArrowLeft\" : \"ArrowUp\"]: () => context.value - context.step,\n    [horizontal ? \"ArrowRight\" : \"ArrowDown\"]: () => context.value + context.step,\n    Home: () => context.min,\n    End: () => context.max,\n    Enter: () => {\n      if (context.value > context.min) {\n        lastExpanded.current = context.value;\n        return context.min;\n      }\n      return lastExpanded.current > context.min ? lastExpanded.current : 50;\n    },\n  };\n  const active = dragging || focused;\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: an <hr> cannot be focusable or expose a value; this is the APG window splitter.\n    <div\n      role=\"separator\"\n      tabIndex={0}\n      aria-label=\"Resize panels\"\n      {...props}\n      aria-orientation={horizontal ? \"vertical\" : \"horizontal\"}\n      aria-valuenow={context.value}\n      aria-valuemin={context.min}\n      aria-valuemax={context.max}\n      aria-controls={`${context.id}-primary`}\n      data-dragging={dragging || undefined}\n      data-slot=\"split-pane-handle\"\n      className={cn(\n        \"group/split-handle relative z-1 grid flex-none touch-none place-items-center outline-none\",\n        horizontal ? \"h-full w-3 cursor-col-resize\" : \"h-3 w-full cursor-row-resize\",\n        context.variant === \"inset\" ? \"m-0\" : horizontal ? \"-mx-1.5\" : \"-my-1.5\",\n        className,\n      )}\n      onFocus={() => setFocused(true)}\n      onBlur={() => setFocused(false)}\n      onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {\n        onKeyDown?.(event);\n        const next = keys[event.key];\n        if (event.defaultPrevented || !next) return;\n        event.preventDefault();\n        context.change(next());\n      }}\n      onPointerDown={(event) => {\n        onPointerDown?.(event);\n        if (event.defaultPrevented || event.button !== 0) return;\n        event.preventDefault();\n        event.currentTarget.setPointerCapture?.(event.pointerId);\n        event.currentTarget.focus();\n        setDragging(true);\n      }}\n      onPointerMove={(event) => {\n        onPointerMove?.(event);\n        if (dragging) context.change(toPercent(event));\n      }}\n      onPointerUp={(event) => {\n        onPointerUp?.(event);\n        if (!dragging) return;\n        event.currentTarget.releasePointerCapture?.(event.pointerId);\n        setDragging(false);\n      }}\n    >\n      <span\n        aria-hidden=\"true\"\n        data-slot=\"split-pane-handle-line\"\n        className={cn(\n          \"[transition:background-color_120ms_ease-out,transform_140ms_ease-out] motion-reduce:transition-none\",\n          horizontal ? \"h-full w-px\" : \"h-px w-full\",\n          focused\n            ? \"bg-ring\"\n            : dragging\n              ? \"bg-border-strong\"\n              : context.variant === \"inset\"\n                ? \"bg-transparent\"\n                : \"bg-border\",\n          active && (horizontal ? \"[transform:scaleX(2)]\" : \"[transform:scaleY(2)]\"),\n        )}\n      />\n      <span\n        aria-hidden=\"true\"\n        data-slot=\"split-pane-handle-grip\"\n        className={cn(\n          \"absolute rounded-full [transition:background-color_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\",\n          horizontal ? \"h-7 w-1\" : \"h-1 w-7\",\n          active\n            ? \"bg-foreground\"\n            : \"bg-border-strong group-hover/split-handle:bg-muted-foreground motion-safe:group-hover/split-handle:scale-110\",\n          context.variant !== \"inset\" && \"shadow-[0_0_0_2px_var(--card)]\",\n          dragging && \"motion-safe:scale-110\",\n        )}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/split-pane.tsx"
    }
  ],
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}