{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dock-split-pane",
  "type": "registry:component",
  "title": "Split pane",
  "description": "Two panes and the divider between them",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-motion.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/dock/src/divider.tsx",
      "target": "@ui/gpuixui/dock/divider.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport { useRef, useState, type ReactElement } from \"react\";\nimport type { EventPayload, StyleDesc } from \"@gpuix/react\";\nimport { motionTiming } from \"../motion/timing.js\";\nimport { controlFocusRing } from \"./theme.js\";\nimport { DIVIDER, mergeStyle, recipeStyle } from \"./recipe.js\";\nimport { DIVIDER_WIDTH, isDividerKey, type DividerKey } from \"./logic.js\";\nimport { useTheme } from \"./theme.js\";\n\nexport interface DividerProps {\n  readonly orientation: \"horizontal\" | \"vertical\";\n\n  readonly valueText: string;\n\n  readonly label?: string;\n  readonly disabled?: boolean;\n\n  readonly seam?: boolean;\n\n  readonly testId?: string;\n\n  readonly onDragStart: () => void;\n\n  readonly onDragDelta: (delta: number) => void;\n\n  readonly onDragEnd?: () => void;\n\n  readonly onKey: (key: DividerKey) => void;\n}\n\nexport const Divider = ({\n  orientation,\n  valueText,\n  label,\n  disabled,\n  seam = false,\n  testId,\n  onDragStart,\n  onDragDelta,\n  onDragEnd,\n  onKey,\n}: DividerProps): ReactElement => {\n  const theme = useTheme();\n  const origin = useRef<number | null>(null);\n  const [dragging, setDragging] = useState(false);\n\n  const axis = (event: EventPayload): number =>\n    orientation === \"horizontal\" ? (event.x ?? 0) : (event.y ?? 0);\n\n  const geometry: StyleDesc =\n    orientation === \"horizontal\"\n      ? { width: DIVIDER_WIDTH, cursor: \"col-resize\" }\n      : { height: DIVIDER_WIDTH, cursor: \"row-resize\" };\n\n  const placement: StyleDesc = seam\n    ? { position: \"absolute\", top: 0, right: 0, bottom: 0 }\n    : mergeStyle(mergeStyle(recipeStyle(DIVIDER, theme), geometry), {\n        flexShrink: 0,\n        flexGrow: 0,\n        position: \"relative\",\n      });\n  const style = mergeStyle(\n    mergeStyle(placement, geometry),\n    controlFocusRing(theme, { disabled }),\n  );\n\n  const highlight: StyleDesc = {\n    position: \"absolute\",\n    top: 0,\n    right: 0,\n    bottom: 0,\n    left: 0,\n    backgroundColor: theme.colors.primary,\n    pointerEvents: \"none\",\n  };\n\n  return (\n    <div\n      role=\"separator\"\n      aria-label={label}\n      aria-valuetext={valueText}\n      tabIndex={disabled === true ? undefined : 0}\n      testId={testId}\n      style={style}\n      onMouseDown={(event) => {\n        if (disabled === true) return;\n        origin.current = axis(event);\n        setDragging(true);\n        onDragStart();\n      }}\n      onMouseMove={(event) => {\n        if (disabled === true || origin.current === null) return;\n        if (event.pressedButton !== 0) {\n          origin.current = null;\n          setDragging(false);\n          onDragEnd?.();\n          return;\n        }\n        onDragDelta(axis(event) - origin.current);\n      }}\n      onMouseUp={() => {\n        if (origin.current === null) return;\n        origin.current = null;\n        setDragging(false);\n        onDragEnd?.();\n      }}\n      onKeyDown={(event) => {\n        if (disabled === true) return;\n        const key = event.key;\n        if (key !== undefined && isDividerKey(key)) onKey(key);\n      }}\n    >\n      {seam ? null : (\n        <div\n          style={highlight}\n          motion={{\n            animate: { opacity: dragging ? 1 : 0 },\n            transition: motionTiming(\"fast\", { phase: \"enter\" }),\n          }}\n        />\n      )}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/dock/src/logic.ts",
      "target": "@ui/gpuixui/dock/logic.ts",
      "type": "registry:lib",
      "content": "import { Effect, Match, pipe } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Number from \"effect/Number\";\nimport * as Schema from \"effect/Schema\";\n\nexport const DIVIDER_STEP = 16;\n\nexport const DIVIDER_WIDTH = 6;\n\nexport interface SizeBounds {\n  readonly min: number;\n  readonly max: number;\n}\n\nexport const sizeBounds = (min = 0, max = Infinity): SizeBounds => {\n  const floor = Math.max(0, min);\n  return { min: floor, max: Math.max(floor, max) };\n};\n\nexport const clampSize = (size: number, bounds: SizeBounds): number =>\n  Number.clamp({ minimum: bounds.min, maximum: bounds.max })(size);\n\nexport const dragSize = (\n  start: number,\n  delta: number,\n  bounds: SizeBounds,\n): number => clampSize(start + delta, bounds);\n\nexport const dividerKeys = [\n  \"left\",\n  \"right\",\n  \"up\",\n  \"down\",\n  \"home\",\n  \"end\",\n] as const;\n\nexport const DividerKey = Schema.Literals(dividerKeys);\n\nexport type DividerKey = typeof DividerKey.Type;\n\nexport const isDividerKey = Schema.is(DividerKey);\n\nexport const Orientation = Schema.Literals([\"horizontal\", \"vertical\"]);\n\nexport type Orientation = typeof Orientation.Type;\n\nexport const resizeByKey = (\n  size: number,\n  key: DividerKey,\n  bounds: SizeBounds,\n  step: number = DIVIDER_STEP,\n): number =>\n  pipe(\n    Match.value(key),\n    Match.when(\"left\", () => clampSize(size - step, bounds)),\n    Match.when(\"up\", () => clampSize(size - step, bounds)),\n    Match.when(\"right\", () => clampSize(size + step, bounds)),\n    Match.when(\"down\", () => clampSize(size + step, bounds)),\n    Match.when(\"home\", () => bounds.min),\n    Match.when(\"end\", () => bounds.max),\n    Match.exhaustive,\n  );\n\nconst DEFAULT_FALSE = Schema.Boolean.pipe(\n  Schema.withDecodingDefault(Effect.succeed(false)),\n  Schema.withConstructorDefault(Effect.succeed(false)),\n);\n\nexport class Panel extends Schema.Class<Panel>(\"Panel\")({\n  id: Schema.NonEmptyString,\n  label: Schema.NonEmptyString,\n  size: Schema.Number,\n  collapsible: DEFAULT_FALSE,\n  collapsed: DEFAULT_FALSE,\n}) {}\n\nexport class PanelLayout extends Schema.Class<PanelLayout>(\"PanelLayout\")({\n  orientation: Orientation,\n  panels: Schema.Array(Panel),\n}) {}\n\nexport const PanelLayoutJson = Schema.fromJsonString(PanelLayout);\n\nexport const normalizePanels = (\n  panels: ReadonlyArray<Panel>,\n): ReadonlyArray<Panel> => {\n  const total = Arr.reduce(\n    panels,\n    0,\n    (sum, panel) => sum + Math.max(0, panel.size),\n  );\n  return total <= 0\n    ? panels\n    : Arr.map(panels, (panel) => ({\n        ...panel,\n        size: Math.max(0, panel.size) / total,\n      }));\n};\n\nexport const togglePanelCollapsed = (\n  layout: PanelLayout,\n  id: string,\n): PanelLayout => {\n  const has = Arr.some(\n    layout.panels,\n    (panel) => panel.id === id && panel.collapsible,\n  );\n  if (!has) return layout;\n  return new PanelLayout({\n    ...layout,\n    panels: Arr.map(layout.panels, (panel) =>\n      panel.id === id\n        ? new Panel({ ...panel, collapsed: !panel.collapsed })\n        : panel,\n    ),\n  });\n};\n\nexport const resizePanels = (\n  panels: ReadonlyArray<Panel>,\n  index: number,\n  delta: number,\n): ReadonlyArray<Panel> => {\n  const first = panels[index];\n  const second = panels[index + 1];\n  if (first === undefined || second === undefined) return panels;\n  const moved = clampSize(first.size + delta, {\n    min: 0,\n    max: first.size + second.size,\n  });\n  if (moved === first.size) return panels;\n  return Arr.map(panels, (panel, at) =>\n    at === index\n      ? new Panel({ ...first, size: moved })\n      : at === index + 1\n        ? new Panel({ ...second, size: first.size + second.size - moved })\n        : panel,\n  );\n};\n\nexport const resizePanelsByKey = (\n  panels: ReadonlyArray<Panel>,\n  index: number,\n  key: DividerKey,\n  step: number,\n): ReadonlyArray<Panel> =>\n  pipe(\n    Match.value(key),\n    Match.when(\"left\", () => resizePanels(panels, index, -step)),\n    Match.when(\"up\", () => resizePanels(panels, index, -step)),\n    Match.when(\"right\", () => resizePanels(panels, index, step)),\n    Match.when(\"down\", () => resizePanels(panels, index, step)),\n    Match.when(\"home\", () => resizePanels(panels, index, -Infinity)),\n    Match.when(\"end\", () => resizePanels(panels, index, Infinity)),\n    Match.exhaustive,\n  );\n\nexport class DockTab extends Schema.Class<DockTab>(\"DockTab\")({\n  id: Schema.NonEmptyString,\n  label: Schema.NonEmptyString,\n}) {}\n\nexport interface DockSplit {\n  readonly kind: \"split\";\n  readonly id: string;\n  readonly orientation: Orientation;\n  readonly size: number;\n  readonly first: DockNode;\n  readonly second: DockNode;\n}\n\nexport interface DockTabs {\n  readonly kind: \"tabs\";\n  readonly id: string;\n  readonly activeTab: string;\n  readonly tabs: ReadonlyArray<DockTab>;\n}\n\nexport type DockNode = DockSplit | DockTabs;\n\nconst splitSize = Schema.Number.pipe(\n  Schema.withConstructorDefault(Effect.succeed(0.5)),\n);\n\nexport const DockSplit: Schema.Codec<DockSplit> = Schema.Struct({\n  kind: Schema.tag(\"split\"),\n  id: Schema.NonEmptyString,\n  orientation: Orientation,\n  size: splitSize,\n  first: Schema.suspend((): Schema.Codec<DockNode> => DockNode),\n  second: Schema.suspend((): Schema.Codec<DockNode> => DockNode),\n});\n\nexport const DockTabs: Schema.Codec<DockTabs> = Schema.Struct({\n  kind: Schema.tag(\"tabs\"),\n  id: Schema.NonEmptyString,\n  activeTab: Schema.NonEmptyString,\n  tabs: Schema.Array(DockTab),\n});\n\nexport const DockNode: Schema.Codec<DockNode> = Schema.Union([\n  DockSplit,\n  DockTabs,\n]).pipe(Schema.toTaggedUnion(\"kind\"));\n\nexport const DockNodeJson = Schema.fromJsonString(DockNode);\n\nexport const resizeSplit = (size: number, delta: number): number =>\n  clampSize(size + delta, { min: 0, max: 1 });\n\nexport const resizeSplitByKey = (\n  size: number,\n  key: DividerKey,\n  step: number,\n): number =>\n  pipe(\n    Match.value(key),\n    Match.when(\"left\", () => resizeSplit(size, -step)),\n    Match.when(\"up\", () => resizeSplit(size, -step)),\n    Match.when(\"right\", () => resizeSplit(size, step)),\n    Match.when(\"down\", () => resizeSplit(size, step)),\n    Match.when(\"home\", () => resizeSplit(size, -Infinity)),\n    Match.when(\"end\", () => resizeSplit(size, Infinity)),\n    Match.exhaustive,\n  );\n\nexport const setActiveTab = (\n  node: DockNode,\n  groupId: string,\n  tabId: string,\n): DockNode => {\n  if (node.kind === \"tabs\") {\n    if (node.id !== groupId || node.activeTab === tabId) return node;\n    if (!Arr.some(node.tabs, (tab) => tab.id === tabId)) return node;\n    return { ...node, activeTab: tabId };\n  }\n  const first = setActiveTab(node.first, groupId, tabId);\n  const second = setActiveTab(node.second, groupId, tabId);\n  if (first === node.first && second === node.second) return node;\n  return { ...node, first, second };\n};\n"
    },
    {
      "path": "packages/dock/src/recipe.ts",
      "target": "@ui/gpuixui/dock/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { classesToStyle } from \"../tw/index.js\";\nimport * as MutableHashMap from \"effect/MutableHashMap\";\nimport * as Option from \"effect/Option\";\nimport type { Theme } from \"./theme.js\";\n\nexport const PANEL_ROOT = \"flex min-h-0 min-w-0\";\n\nexport const PANE = \"min-h-0 min-w-0 overflow-hidden\";\n\nexport const DIVIDER = \"bg-border hover:bg-primary\";\n\nexport const DOCK_ROOT = \"flex min-h-0 min-w-0\";\n\nexport const DOCK_PANE = \"min-h-0 min-w-0 overflow-hidden\";\n\nconst CACHE_LIMIT = 256;\n\nconst compiled = new WeakMap<\n  Theme,\n  MutableHashMap.MutableHashMap<string, StyleDesc>\n>();\n\nexport const recipeStyle = (recipe: string, theme: Theme): StyleDesc => {\n  const byRecipe = compiled.get(theme);\n  if (byRecipe !== undefined) {\n    const hit = MutableHashMap.get(byRecipe, recipe);\n    if (Option.isSome(hit)) return hit.value;\n  }\n  const style = Object.freeze(classesToStyle(recipe, theme).style);\n  if (byRecipe === undefined) {\n    compiled.set(theme, MutableHashMap.make([recipe, style]));\n  } else if (MutableHashMap.size(byRecipe) < CACHE_LIMIT) {\n    MutableHashMap.set(byRecipe, recipe, style);\n  }\n  return style;\n};\n\nexport const mergeStyle = (\n  base: StyleDesc,\n  override: StyleDesc | undefined,\n): StyleDesc => (override === undefined ? base : { ...base, ...override });\n"
    },
    {
      "path": "packages/dock/src/split-pane.tsx",
      "target": "@ui/gpuixui/dock/split-pane.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useRef } from \"react\";\nimport { cn } from \"../tw/index.js\";\nimport { Divider } from \"./divider.js\";\nimport { dragSize, resizeByKey, sizeBounds } from \"./logic.js\";\nimport { PANE, PANEL_ROOT, mergeStyle, recipeStyle } from \"./recipe.js\";\nimport { useTheme } from \"./theme.js\";\n\nexport interface SplitPaneProps {\n  readonly orientation?: \"horizontal\" | \"vertical\";\n\n  readonly size: number;\n\n  readonly onResize?: (size: number) => void;\n\n  readonly min?: number;\n\n  readonly max?: number;\n\n  readonly first: ReactNode;\n\n  readonly second: ReactNode;\n\n  readonly dividerLabel?: string;\n  readonly disabled?: boolean;\n\n  readonly className?: string;\n\n  readonly testId?: string;\n}\n\nexport const SplitPane = ({\n  orientation = \"horizontal\",\n  size,\n  onResize,\n  min,\n  max,\n  first,\n  second,\n  dividerLabel,\n  disabled,\n  className,\n  testId,\n}: SplitPaneProps): ReactElement => {\n  const theme = useTheme();\n  const bounds = sizeBounds(min, max);\n  const dragStart = useRef(size);\n\n  const root = mergeStyle(recipeStyle(cn(PANEL_ROOT, className), theme), {\n    flexDirection: orientation === \"horizontal\" ? \"row\" : \"column\",\n    alignItems: \"stretch\",\n    flexGrow: 1,\n    flexShrink: 1,\n    flexBasis: 0,\n  });\n\n  const firstPane = mergeStyle(recipeStyle(PANE, theme), {\n    flexGrow: 0,\n    flexShrink: 0,\n    flexBasis: size,\n  });\n  const secondPane = mergeStyle(recipeStyle(PANE, theme), {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexBasis: 0,\n  });\n\n  return (\n    <div testId={testId} style={root}>\n      <div style={firstPane}>{first}</div>\n      <Divider\n        orientation={orientation}\n        valueText={`${Math.round(size)} px`}\n        label={dividerLabel}\n        disabled={disabled}\n        testId={testId === undefined ? undefined : `${testId}-divider`}\n        onDragStart={() => {\n          dragStart.current = size;\n        }}\n        onDragDelta={(delta) => {\n          onResize?.(dragSize(dragStart.current, delta, bounds));\n        }}\n        onKey={(key) => {\n          onResize?.(resizeByKey(size, key, bounds));\n        }}\n      />\n      <div style={secondPane}>{second}</div>\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/dock/src/theme.ts",
      "target": "@ui/gpuixui/dock/theme.ts",
      "type": "registry:lib",
      "content": "import type { Theme } from \"../tokens/index.js\";\nimport { useTheme as useTokenTheme } from \"../tokens/react.js\";\n\nexport {\n  controlFocusRing,\n  keyboardFocusRing,\n  textStyle,\n} from \"../tokens/index.js\";\nexport type { Theme };\n\nexport const useTheme = (): Theme => useTokenTheme();\n"
    }
  ],
  "docs": "[Split pane](https://gpuixui.sudokw.com/dock/split-pane) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/dock/split-pane",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}