{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-workspace",
  "title": "Split Workspace",
  "author": "ilinxa",
  "description": "Splittable, mergeable canvas of editor areas — a dynamic layout primitive for dashboards, dev tools, and data apps.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "dropdown-menu",
    "scroll-area",
    "tabs"
  ],
  "files": [
    {
      "path": "src/registry/components/layout/split-workspace/split-workspace.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useReducer,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Area } from \"./parts/area\";\nimport { CardStack } from \"./parts/card-stack\";\nimport { DragOverlay } from \"./parts/drag-overlay\";\nimport { PresetsTabs } from \"./parts/presets-tabs\";\nimport { SplitDivider } from \"./parts/split-divider\";\nimport { useBreakpoint } from \"./hooks/use-breakpoint\";\nimport { useAreaFocus } from \"./hooks/use-area-focus\";\nimport { useCornerGesture } from \"./hooks/use-corner-gesture\";\nimport { useEdgeGesture } from \"./hooks/use-edge-gesture\";\nimport {\n  useKeyboardActions,\n  useResizeKeyboard,\n} from \"./hooks/use-keyboard-actions\";\nimport { reducer, type Action } from \"./lib/reducer\";\nimport {\n  computeLayout,\n  computeLayoutBoundsForPath,\n  flattenLeavesInOrder,\n  flattenSubtreesPastDepth,\n  getNodeAtPath,\n  treeDepth,\n  validateTree,\n} from \"./lib/tree\";\nimport { makeAreaId } from \"./lib/ids\";\nimport type {\n  AreaTree,\n  Breakpoint,\n  ResponsiveValue,\n  SplitWorkspaceProps,\n} from \"./types\";\n\nconst DEFAULT_BREAKPOINTS = { mobile: 640, tablet: 1024 };\nconst DEFAULT_MIN_AREA_SIZE = { width: 120, height: 80 };\nconst DEFAULT_MAX_DEPTH: { mobile: number; tablet: number; desktop: number } = {\n  mobile: 0,\n  tablet: 3,\n  desktop: 7,\n};\n\nfunction resolveResponsive<T>(\n  value: ResponsiveValue<T> | undefined,\n  breakpoint: Breakpoint,\n  fallback: { mobile: T; tablet: T; desktop: T },\n): T {\n  if (value === undefined) return fallback[breakpoint];\n  if (typeof value === \"number\" || typeof value === \"string\") {\n    return value as T;\n  }\n  if (typeof value === \"object\" && value !== null) {\n    const v = value as { mobile?: T; tablet?: T; desktop?: T };\n    return v[breakpoint] ?? fallback[breakpoint];\n  }\n  return value as T;\n}\n\nfunction createInitialTree(\n  defaultComponentId: string,\n  rootAreaId: string,\n): AreaTree {\n  // v0.1.4 (review): the root id is now deterministic (useId-derived, passed\n  // in) instead of `makeAreaId()` — the lazy initializer runs during render\n  // on BOTH server and client, and the old Date.now()-based id was emitted\n  // as `data-area-id`, guaranteeing an SSR hydration mismatch for the\n  // default tree. Event-driven splits still use `makeAreaId()` (client-only).\n  return { kind: \"leaf\", id: rootAreaId, componentId: defaultComponentId };\n}\n\nexport function SplitWorkspace({\n  components,\n  defaultComponentId,\n  layout,\n  defaultLayout,\n  onLayoutChange,\n  presets,\n  activePresetId: activePresetIdProp,\n  onActivePresetChange,\n  minAreaSize = DEFAULT_MIN_AREA_SIZE,\n  maxSplitDepth,\n  breakpoints = DEFAULT_BREAKPOINTS,\n  cardStackItemHeight,\n  onError,\n  className,\n  \"aria-label\": ariaLabel = \"Split Workspace\",\n}: SplitWorkspaceProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLDivElement>(null);\n\n  // SSR-stable seed for the default tree's root area id (see createInitialTree).\n  const reactId = useId();\n\n  const [{ tree, focusedAreaId }, dispatchInternal] = useReducer(reducer, null, () => ({\n    tree:\n      layout ??\n      defaultLayout ??\n      createInitialTree(defaultComponentId, `area-${reactId}`),\n    focusedAreaId: null,\n  }));\n\n  // v0.1.4 (review 5.5): tag prop-originated `replace-tree` dispatches so the\n  // notify effect below can skip them. Without the tag, a controlled parent\n  // that reverts (or hasn't yet accepted) an edit produced this sequence:\n  // user edit → onLayoutChange(T1) → prop-driven revert → onLayoutChange(L0)\n  // — the edit was announced and then un-announced.\n  const propReplaceRef = useRef<AreaTree | null>(null);\n\n  // Sync controlled `layout` prop into reducer state by reference comparison.\n  useEffect(() => {\n    if (layout !== undefined && layout !== tree) {\n      propReplaceRef.current = layout;\n      dispatchInternal({ type: \"replace-tree\", tree: layout });\n    }\n  }, [layout, tree]);\n\n  // Fire onLayoutChange when tree changes (excluding initial mount and\n  // prop-originated replaces — the consumer already knows about those).\n  const onLayoutChangeRef = useRef(onLayoutChange);\n  useEffect(() => {\n    onLayoutChangeRef.current = onLayoutChange;\n  }, [onLayoutChange]);\n  const prevTreeRef = useRef(tree);\n  useEffect(() => {\n    if (prevTreeRef.current !== tree) {\n      prevTreeRef.current = tree;\n      if (propReplaceRef.current === tree) {\n        propReplaceRef.current = null;\n        return;\n      }\n      onLayoutChangeRef.current?.(tree);\n    }\n  }, [tree]);\n\n  const dispatch = useCallback<React.Dispatch<Action>>((action) => {\n    dispatchInternal(action);\n  }, []);\n\n  // Active preset state — controlled if prop provided, otherwise internal.\n  const [internalActivePresetId, setInternalActivePresetId] = useState<\n    string | null\n  >(presets && presets.length > 0 ? (presets[0]?.id ?? null) : null);\n  const activePresetId =\n    activePresetIdProp !== undefined\n      ? activePresetIdProp\n      : internalActivePresetId;\n\n  const handlePresetChange = useCallback(\n    (id: string) => {\n      const preset = presets?.find((p) => p.id === id);\n      if (!preset) return;\n      dispatch({ type: \"replace-tree\", tree: preset.layout });\n      if (activePresetIdProp === undefined) {\n        setInternalActivePresetId(id);\n      }\n      onActivePresetChange?.(id);\n    },\n    [presets, dispatch, activePresetIdProp, onActivePresetChange],\n  );\n\n  // Validate tree once per change for unregistered component-ids etc.\n  const onErrorRef = useRef(onError);\n  useEffect(() => {\n    onErrorRef.current = onError;\n  }, [onError]);\n  useEffect(() => {\n    const componentIds = components.map((c) => c.id);\n    const result = validateTree(tree, componentIds);\n    if (!result.valid) {\n      if (process.env.NODE_ENV !== \"production\") {\n        console.error(\"[split-workspace] invalid tree:\", result.errors);\n      }\n      onErrorRef.current?.(result.errors);\n    }\n  }, [tree, components]);\n\n  // Track breakpoint via ResizeObserver on canvas.\n  const breakpoint = useBreakpoint(canvasRef, breakpoints);\n\n  const cap = resolveResponsive(maxSplitDepth, breakpoint, DEFAULT_MAX_DEPTH);\n\n  const renderedTree = useMemo(\n    () =>\n      cap >= treeDepth(tree)\n        ? tree\n        : flattenSubtreesPastDepth(tree, Math.max(0, cap)),\n    [tree, cap],\n  );\n\n  const isStacked = breakpoint === \"mobile\";\n\n  // Measure canvas to compute layout rects.\n  const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });\n  useEffect(() => {\n    const el = canvasRef.current;\n    if (!el) return;\n    const observer = new ResizeObserver(() => {\n      setCanvasSize({ width: el.clientWidth, height: el.clientHeight });\n    });\n    observer.observe(el);\n    setCanvasSize({ width: el.clientWidth, height: el.clientHeight });\n    return () => observer.disconnect();\n  }, []);\n\n  const { leaves, dividers } = useMemo(() => {\n    if (isStacked || canvasSize.width === 0 || canvasSize.height === 0) {\n      return { leaves: [], dividers: [] };\n    }\n    return computeLayout(\n      renderedTree,\n      { x: 0, y: 0, width: canvasSize.width, height: canvasSize.height },\n      { minSize: minAreaSize },\n    );\n  }, [renderedTree, canvasSize, isStacked, minAreaSize]);\n\n  const stackedLeaves = useMemo(\n    () => (isStacked ? flattenLeavesInOrder(tree) : []),\n    [tree, isStacked],\n  );\n\n  const handleFocusChange = useCallback(\n    (areaId: string | null) => {\n      dispatch({ type: \"focus\", areaId });\n    },\n    [dispatch],\n  );\n  useAreaFocus(rootRef, handleFocusChange);\n\n  const { dragState, beginCornerDrag } = useCornerGesture({\n    rootRef: canvasRef,\n    leaves,\n    cap,\n    breakpoint,\n    dispatch,\n    makeAreaId,\n  });\n\n  const { beginEdgeDrag, isResizing } = useEdgeGesture({\n    rootRef: canvasRef,\n    minAreaSize,\n    dispatch,\n  });\n\n  const keyboard = useKeyboardActions({\n    leaves,\n    focusedAreaId,\n    cap,\n    dispatch,\n    makeAreaId,\n  });\n\n  const handleCornerPointerDown = useCallback(\n    (areaId: string, e: React.PointerEvent<HTMLDivElement>) => {\n      beginCornerDrag(areaId, e.clientX, e.clientY, e.pointerId);\n    },\n    [beginCornerDrag],\n  );\n\n  const handleDividerPointerDown = useCallback(\n    (\n      splitPath: number[],\n      e: React.PointerEvent<HTMLDivElement>,\n    ) => {\n      const node = getNodeAtPath(renderedTree, splitPath);\n      if (node.kind !== \"split\") return;\n      if (!canvasRef.current) return;\n      const parentRect = computeLayoutBoundsForPath(\n        renderedTree,\n        splitPath,\n        canvasSize,\n        minAreaSize,\n      );\n      if (!parentRect) return;\n      beginEdgeDrag(\n        splitPath,\n        node.orientation,\n        parentRect,\n        node.ratio,\n        e.clientX,\n        e.clientY,\n        e.pointerId,\n      );\n    },\n    [renderedTree, canvasSize, minAreaSize, beginEdgeDrag],\n  );\n\n  const handleAreaSelectComponent = useCallback(\n    (areaId: string, componentId: string) => {\n      dispatch({ type: \"swap\", areaId, componentId });\n    },\n    [dispatch],\n  );\n\n  const handleStackedSelectComponent = useCallback(\n    (areaId: string, componentId: string) => {\n      dispatch({ type: \"swap\", areaId, componentId });\n    },\n    [dispatch],\n  );\n\n  useResizeKeyboard({\n    enabled: !isStacked,\n    leaves,\n    dividers,\n    renderedTree,\n    focusedAreaId,\n    dispatch,\n  });\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"application\"\n      aria-label={ariaLabel}\n      className={cn(\n        \"relative flex h-full w-full flex-col overflow-hidden rounded-md border border-border bg-background text-foreground\",\n        className,\n      )}\n    >\n      {presets && presets.length > 0 ? (\n        <PresetsTabs\n          presets={presets}\n          activePresetId={activePresetId}\n          onChange={handlePresetChange}\n        />\n      ) : null}\n      <div\n        ref={canvasRef}\n        className={cn(\n          \"relative flex-1 overflow-hidden\",\n          (isResizing || dragState !== null) && \"select-none\",\n        )}\n      >\n        {isStacked ? (\n          <CardStack\n            leaves={stackedLeaves}\n            components={components}\n            itemHeight={cardStackItemHeight}\n            onSelectComponent={handleStackedSelectComponent}\n          />\n        ) : (\n          <>\n            {leaves.map((leaf) => (\n              <div\n                key={leaf.areaId}\n                className=\"absolute\"\n                style={{\n                  left: leaf.x,\n                  top: leaf.y,\n                  width: leaf.width,\n                  height: leaf.height,\n                }}\n              >\n                <Area\n                  areaId={leaf.areaId}\n                  componentId={leaf.componentId}\n                  components={components}\n                  width={leaf.width}\n                  height={leaf.height}\n                  isFocused={focusedAreaId === leaf.areaId}\n                  cornerInert={leaf.depth >= cap}\n                  canSplit={leaf.depth < cap}\n                  mergeOptions={{\n                    left: keyboard.findNeighbor(leaf.areaId, \"left\") !== null,\n                    right: keyboard.findNeighbor(leaf.areaId, \"right\") !== null,\n                    up: keyboard.findNeighbor(leaf.areaId, \"up\") !== null,\n                    down: keyboard.findNeighbor(leaf.areaId, \"down\") !== null,\n                  }}\n                  onCornerPointerDown={handleCornerPointerDown}\n                  onSelectComponent={(componentId) =>\n                    handleAreaSelectComponent(leaf.areaId, componentId)\n                  }\n                  onSplitVertical={() => {\n                    if (focusedAreaId !== leaf.areaId) {\n                      dispatch({ type: \"focus\", areaId: leaf.areaId });\n                    }\n                    if (leaf.depth < cap) {\n                      dispatch({\n                        type: \"split\",\n                        areaId: leaf.areaId,\n                        orientation: \"vertical\",\n                        newAreaId: makeAreaId(),\n                      });\n                    }\n                  }}\n                  onSplitHorizontal={() => {\n                    if (focusedAreaId !== leaf.areaId) {\n                      dispatch({ type: \"focus\", areaId: leaf.areaId });\n                    }\n                    if (leaf.depth < cap) {\n                      dispatch({\n                        type: \"split\",\n                        areaId: leaf.areaId,\n                        orientation: \"horizontal\",\n                        newAreaId: makeAreaId(),\n                      });\n                    }\n                  }}\n                  onMergeDirection={(dir) => {\n                    if (focusedAreaId !== leaf.areaId) {\n                      dispatch({ type: \"focus\", areaId: leaf.areaId });\n                    }\n                    const neighbor = keyboard.findNeighbor(leaf.areaId, dir);\n                    if (neighbor) {\n                      dispatch({\n                        type: \"merge\",\n                        survivorId: leaf.areaId,\n                        absorbedId: neighbor.areaId,\n                      });\n                    }\n                  }}\n                  onAreaPointerDown={() =>\n                    dispatch({ type: \"focus\", areaId: leaf.areaId })\n                  }\n                />\n              </div>\n            ))}\n            {dividers.map((d) => {\n              const node = getNodeAtPath(renderedTree, d.splitPath);\n              const ratio = node.kind === \"split\" ? node.ratio : 0.5;\n              return (\n                <SplitDivider\n                  key={d.splitPath.join(\"-\")}\n                  orientation={d.orientation}\n                  x={d.x}\n                  y={d.y}\n                  length={d.length}\n                  ratioPercent={ratio * 100}\n                  onPointerDown={(e) => handleDividerPointerDown(d.splitPath, e)}\n                  onKeyResize={(delta) => {\n                    const currentNode = getNodeAtPath(renderedTree, d.splitPath);\n                    if (currentNode.kind !== \"split\") return;\n                    const next = Math.max(\n                      0.05,\n                      Math.min(\n                        0.95,\n                        currentNode.ratio +\n                          keyboard.KEYBOARD_RESIZE_STEP * delta,\n                      ),\n                    );\n                    dispatch({\n                      type: \"resize\",\n                      splitPath: d.splitPath,\n                      ratio: next,\n                    });\n                  }}\n                />\n              );\n            })}\n            <DragOverlay dragState={dragState} leaves={leaves} />\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n\n",
      "type": "registry:component",
      "target": "components/split-workspace/split-workspace.tsx"
    },
    {
      "path": "src/registry/components/layout/split-workspace/index.ts",
      "content": "export { SplitWorkspace } from \"./split-workspace\";\nexport { useAreaContext } from \"./hooks/use-area-context\";\nexport type {\n  AreaContext,\n  AreaTree,\n  AreaTreeLeaf,\n  AreaTreeSplit,\n  Breakpoint,\n  ResponsiveValue,\n  SplitOrientation,\n  SplitWorkspaceComponent,\n  SplitWorkspacePreset,\n  SplitWorkspaceProps,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/split-workspace/index.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/types.ts",
      "content": "import type { ReactNode } from \"react\";\n\nexport type ResponsiveValue<T> = T | { mobile?: T; tablet?: T; desktop?: T };\n\nexport type Breakpoint = \"mobile\" | \"tablet\" | \"desktop\";\n\nexport type SplitWorkspaceComponent = {\n  id: string;\n  name: string;\n  icon?: ReactNode;\n  category?: string;\n  render: () => ReactNode;\n};\n\nexport type AreaContext = {\n  areaId: string;\n  width: number;\n  height: number;\n  isFocused: boolean;\n};\n\nexport type SplitOrientation = \"vertical\" | \"horizontal\";\n\nexport type AreaTreeLeaf = {\n  kind: \"leaf\";\n  id: string;\n  componentId: string;\n};\n\nexport type AreaTreeSplit = {\n  kind: \"split\";\n  orientation: SplitOrientation;\n  ratio: number;\n  a: AreaTree;\n  b: AreaTree;\n};\n\nexport type AreaTree = AreaTreeLeaf | AreaTreeSplit;\n\nexport type SplitWorkspacePreset = {\n  id: string;\n  name: string;\n  layout: AreaTree;\n};\n\nexport type SplitWorkspaceProps = {\n  components: SplitWorkspaceComponent[];\n  defaultComponentId: string;\n\n  layout?: AreaTree;\n  defaultLayout?: AreaTree;\n  onLayoutChange?: (next: AreaTree) => void;\n\n  presets?: SplitWorkspacePreset[];\n  activePresetId?: string;\n  onActivePresetChange?: (id: string) => void;\n\n  minAreaSize?: { width: number; height: number };\n  maxSplitDepth?: ResponsiveValue<number>;\n  breakpoints?: { mobile: number; tablet: number };\n\n  cardStackItemHeight?: number;\n\n  onError?: (errors: string[]) => void;\n\n  \"aria-label\"?: string;\n  className?: string;\n};\n",
      "type": "registry:component",
      "target": "components/split-workspace/types.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/hooks/use-area-context.ts",
      "content": "\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport type { AreaContext } from \"../types\";\n\nexport const AreaContextContext = createContext<AreaContext | null>(null);\n\nexport function useAreaContext(): AreaContext {\n  const ctx = useContext(AreaContextContext);\n  if (!ctx) {\n    throw new Error(\n      \"useAreaContext must be called inside a SplitWorkspace area's render() subtree\",\n    );\n  }\n  return ctx;\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/hooks/use-area-context.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/hooks/use-area-focus.ts",
      "content": "\"use client\";\n\nimport { useEffect } from \"react\";\n\nexport function useAreaFocus(\n  rootRef: React.RefObject<HTMLElement | null>,\n  onFocusChange: (areaId: string | null) => void,\n) {\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n\n    const findAreaId = (el: Element | null): string | null => {\n      let current: Element | null = el;\n      while (current && current !== root) {\n        if (current instanceof HTMLElement && current.dataset.areaId) {\n          return current.dataset.areaId;\n        }\n        current = current.parentElement;\n      }\n      return null;\n    };\n\n    const handleFocusIn = (e: FocusEvent) => {\n      const areaId = findAreaId(e.target as Element | null);\n      onFocusChange(areaId);\n    };\n\n    const handleFocusOut = (e: FocusEvent) => {\n      const next = e.relatedTarget as Element | null;\n      if (!next || !root.contains(next)) {\n        onFocusChange(null);\n      }\n    };\n\n    root.addEventListener(\"focusin\", handleFocusIn);\n    root.addEventListener(\"focusout\", handleFocusOut);\n    return () => {\n      root.removeEventListener(\"focusin\", handleFocusIn);\n      root.removeEventListener(\"focusout\", handleFocusOut);\n    };\n  }, [rootRef, onFocusChange]);\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/hooks/use-area-focus.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/hooks/use-breakpoint.ts",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport type { Breakpoint } from \"../types\";\n\nexport function useBreakpoint(\n  rootRef: React.RefObject<HTMLElement | null>,\n  breakpoints: { mobile: number; tablet: number },\n  debounceMs = 100,\n): Breakpoint {\n  const [breakpoint, setBreakpoint] = useState<Breakpoint>(\"desktop\");\n  const timeoutRef = useRef<number | null>(null);\n\n  useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n\n    const compute = (width: number): Breakpoint => {\n      if (width < breakpoints.mobile) return \"mobile\";\n      if (width < breakpoints.tablet) return \"tablet\";\n      return \"desktop\";\n    };\n\n    const measure = () => {\n      const width = el.clientWidth;\n      setBreakpoint(compute(width));\n    };\n\n    measure();\n\n    const observer = new ResizeObserver(() => {\n      if (timeoutRef.current !== null) {\n        window.clearTimeout(timeoutRef.current);\n      }\n      timeoutRef.current = window.setTimeout(measure, debounceMs);\n    });\n    observer.observe(el);\n\n    return () => {\n      observer.disconnect();\n      if (timeoutRef.current !== null) {\n        window.clearTimeout(timeoutRef.current);\n      }\n    };\n  }, [rootRef, breakpoints.mobile, breakpoints.tablet, debounceMs]);\n\n  return breakpoint;\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/hooks/use-breakpoint.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/hooks/use-corner-gesture.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { Action } from \"../lib/reducer\";\nimport { inferDragOrientation, pointInRect } from \"../lib/geometry\";\nimport type { LeafRect } from \"../lib/geometry\";\nimport type { SplitOrientation } from \"../types\";\n\nexport type CornerDragState = {\n  originAreaId: string;\n  startX: number;\n  startY: number;\n  currentX: number;\n  currentY: number;\n  intent:\n    | { kind: \"none\" }\n    | {\n        kind: \"split\";\n        orientation: SplitOrientation;\n        position: { x: number; y: number };\n      }\n    | { kind: \"merge\"; targetAreaId: string };\n};\n\nconst DRAG_THRESHOLD_PX = 4;\n\nexport function useCornerGesture(opts: {\n  rootRef: React.RefObject<HTMLElement | null>;\n  leaves: LeafRect[];\n  cap: number;\n  breakpoint: string;\n  dispatch: React.Dispatch<Action>;\n  makeAreaId: () => string;\n}) {\n  const { rootRef, leaves, cap, breakpoint, dispatch, makeAreaId } = opts;\n  const [dragState, setDragState] = useState<CornerDragState | null>(null);\n  const dragRef = useRef<CornerDragState | null>(null);\n  const inertLoggedRef = useRef(false);\n  const leavesRef = useRef(leaves);\n  useEffect(() => {\n    leavesRef.current = leaves;\n  }, [leaves]);\n\n  const beginCornerDrag = useCallback(\n    (areaId: string, clientX: number, clientY: number, pointerId: number) => {\n      const root = rootRef.current;\n      if (!root) return;\n      const rootRect = root.getBoundingClientRect();\n      const localX = clientX - rootRect.left;\n      const localY = clientY - rootRect.top;\n      const initial: CornerDragState = {\n        originAreaId: areaId,\n        startX: localX,\n        startY: localY,\n        currentX: localX,\n        currentY: localY,\n        intent: { kind: \"none\" },\n      };\n      dragRef.current = initial;\n      setDragState(initial);\n      try {\n        root.setPointerCapture(pointerId);\n      } catch {\n        // ignore — some browsers throw if pointer is not active\n      }\n    },\n    [rootRef],\n  );\n\n  // v0.1.4 (review): key the listener effect on WHETHER a drag is active,\n  // not on the dragState object — every pointermove minted a new dragState,\n  // tearing down and re-binding all 4 listeners per move. The handlers read\n  // live drag data from `dragRef` anyway.\n  const dragActive = dragState !== null;\n\n  useEffect(() => {\n    if (!dragActive) return;\n    const root = rootRef.current;\n    if (!root) return;\n\n    const computeIntent = (\n      origin: LeafRect,\n      currentX: number,\n      currentY: number,\n      startX: number,\n      startY: number,\n    ): CornerDragState[\"intent\"] => {\n      const dx = currentX - startX;\n      const dy = currentY - startY;\n      const distance = Math.hypot(dx, dy);\n      if (distance < DRAG_THRESHOLD_PX) return { kind: \"none\" };\n\n      if (pointInRect(currentX, currentY, origin)) {\n        // SPLIT intent — gated by per-leaf depth. Splitting a leaf at depth N\n        // produces children at depth N+1, so the cap rule is `origin.depth < cap`.\n        if (origin.depth >= cap) {\n          if (!inertLoggedRef.current) {\n            console.debug(\n              `[split-workspace] split inert: leaf at depth ${origin.depth} would exceed cap ${cap} for breakpoint \"${breakpoint}\"`,\n            );\n            inertLoggedRef.current = true;\n          }\n          return { kind: \"none\" };\n        }\n        const orientation = inferDragOrientation(\n          startX,\n          startY,\n          currentX,\n          currentY,\n        );\n        const position =\n          orientation === \"vertical\"\n            ? { x: currentX, y: origin.y }\n            : { x: origin.x, y: currentY };\n        return { kind: \"split\", orientation, position };\n      }\n\n      const target = leavesRef.current.find(\n        (l) => l.areaId !== origin.areaId && pointInRect(currentX, currentY, l),\n      );\n      if (target) {\n        return { kind: \"merge\", targetAreaId: target.areaId };\n      }\n      return { kind: \"none\" };\n    };\n\n    const handleMove = (e: PointerEvent) => {\n      const current = dragRef.current;\n      if (!current) return;\n      const rootRect = root.getBoundingClientRect();\n      const localX = e.clientX - rootRect.left;\n      const localY = e.clientY - rootRect.top;\n      const origin = leavesRef.current.find(\n        (l) => l.areaId === current.originAreaId,\n      );\n      if (!origin) return;\n      const intent = computeIntent(\n        origin,\n        localX,\n        localY,\n        current.startX,\n        current.startY,\n      );\n      const next: CornerDragState = {\n        ...current,\n        currentX: localX,\n        currentY: localY,\n        intent,\n      };\n      dragRef.current = next;\n      setDragState(next);\n    };\n\n    const handleUp = (e: PointerEvent) => {\n      const current = dragRef.current;\n      if (!current) {\n        setDragState(null);\n        return;\n      }\n      const intent = current.intent;\n      if (intent.kind === \"split\") {\n        dispatch({\n          type: \"split\",\n          areaId: current.originAreaId,\n          orientation: intent.orientation,\n          newAreaId: makeAreaId(),\n        });\n      } else if (intent.kind === \"merge\") {\n        dispatch({\n          type: \"merge\",\n          survivorId: current.originAreaId,\n          absorbedId: intent.targetAreaId,\n        });\n      }\n      try {\n        root.releasePointerCapture(e.pointerId);\n      } catch {\n        // ignore\n      }\n      dragRef.current = null;\n      setDragState(null);\n    };\n\n    const handleCancel = () => {\n      dragRef.current = null;\n      setDragState(null);\n    };\n\n    root.addEventListener(\"pointermove\", handleMove);\n    root.addEventListener(\"pointerup\", handleUp);\n    root.addEventListener(\"pointercancel\", handleCancel);\n    window.addEventListener(\"blur\", handleCancel);\n    return () => {\n      root.removeEventListener(\"pointermove\", handleMove);\n      root.removeEventListener(\"pointerup\", handleUp);\n      root.removeEventListener(\"pointercancel\", handleCancel);\n      window.removeEventListener(\"blur\", handleCancel);\n    };\n  }, [dragActive, rootRef, dispatch, makeAreaId, cap, breakpoint]);\n\n  return { dragState, beginCornerDrag };\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/hooks/use-corner-gesture.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/hooks/use-edge-gesture.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { Action } from \"../lib/reducer\";\nimport type { Rect } from \"../lib/geometry\";\nimport { clampRatio } from \"../lib/geometry\";\nimport type { SplitOrientation } from \"../types\";\n\ntype EdgeDragState = {\n  splitPath: number[];\n  orientation: SplitOrientation;\n  bounds: Rect;\n  startRatio: number;\n  startClient: { x: number; y: number };\n};\n\nexport function useEdgeGesture(opts: {\n  rootRef: React.RefObject<HTMLElement | null>;\n  minAreaSize: { width: number; height: number };\n  dispatch: React.Dispatch<Action>;\n}) {\n  const { rootRef, minAreaSize, dispatch } = opts;\n  const [active, setActive] = useState(false);\n  const dragRef = useRef<EdgeDragState | null>(null);\n  const rafRef = useRef<number | null>(null);\n  const pendingRatioRef = useRef<number | null>(null);\n\n  const beginEdgeDrag = useCallback(\n    (\n      splitPath: number[],\n      orientation: SplitOrientation,\n      bounds: Rect,\n      startRatio: number,\n      clientX: number,\n      clientY: number,\n      pointerId: number,\n    ) => {\n      const root = rootRef.current;\n      if (!root) return;\n      dragRef.current = {\n        splitPath,\n        orientation,\n        bounds,\n        startRatio,\n        startClient: { x: clientX, y: clientY },\n      };\n      setActive(true);\n      try {\n        root.setPointerCapture(pointerId);\n      } catch {\n        // ignore\n      }\n    },\n    [rootRef],\n  );\n\n  useEffect(() => {\n    if (!active) return;\n    const root = rootRef.current;\n    if (!root) return;\n\n    const flush = () => {\n      rafRef.current = null;\n      const next = pendingRatioRef.current;\n      pendingRatioRef.current = null;\n      const drag = dragRef.current;\n      if (!drag || next === null) return;\n      dispatch({ type: \"resize\", splitPath: drag.splitPath, ratio: next });\n    };\n\n    const handleMove = (e: PointerEvent) => {\n      const drag = dragRef.current;\n      if (!drag) return;\n      const dx = e.clientX - drag.startClient.x;\n      const dy = e.clientY - drag.startClient.y;\n      let nextRatio: number;\n      if (drag.orientation === \"vertical\") {\n        const offset = dx;\n        const aWidth = drag.bounds.width * drag.startRatio + offset;\n        nextRatio = clampRatio(\n          aWidth / drag.bounds.width,\n          drag.bounds.width,\n          minAreaSize.width,\n          minAreaSize.width,\n        );\n      } else {\n        const offset = dy;\n        const aHeight = drag.bounds.height * drag.startRatio + offset;\n        nextRatio = clampRatio(\n          aHeight / drag.bounds.height,\n          drag.bounds.height,\n          minAreaSize.height,\n          minAreaSize.height,\n        );\n      }\n      pendingRatioRef.current = nextRatio;\n      if (rafRef.current === null) {\n        rafRef.current = window.requestAnimationFrame(flush);\n      }\n    };\n\n    const handleUp = (e: PointerEvent) => {\n      if (rafRef.current !== null) {\n        window.cancelAnimationFrame(rafRef.current);\n        rafRef.current = null;\n        flush();\n      }\n      try {\n        root.releasePointerCapture(e.pointerId);\n      } catch {\n        // ignore\n      }\n      dragRef.current = null;\n      setActive(false);\n    };\n\n    const handleCancel = () => {\n      if (rafRef.current !== null) {\n        window.cancelAnimationFrame(rafRef.current);\n        rafRef.current = null;\n      }\n      dragRef.current = null;\n      setActive(false);\n    };\n\n    root.addEventListener(\"pointermove\", handleMove);\n    root.addEventListener(\"pointerup\", handleUp);\n    root.addEventListener(\"pointercancel\", handleCancel);\n    return () => {\n      root.removeEventListener(\"pointermove\", handleMove);\n      root.removeEventListener(\"pointerup\", handleUp);\n      root.removeEventListener(\"pointercancel\", handleCancel);\n    };\n  }, [active, rootRef, dispatch, minAreaSize.width, minAreaSize.height]);\n\n  return { beginEdgeDrag, isResizing: active };\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/hooks/use-edge-gesture.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/hooks/use-keyboard-actions.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect } from \"react\";\nimport type { Action } from \"../lib/reducer\";\nimport type { LeafRect, SplitDividerRect } from \"../lib/geometry\";\nimport { rectsShareFullEdge } from \"../lib/geometry\";\nimport { getNodeAtPath } from \"../lib/tree\";\nimport type { AreaTree, SplitOrientation } from \"../types\";\n\nconst KEYBOARD_RESIZE_STEP = 0.04;\n\ntype ResizeDirection = \"left\" | \"right\" | \"up\" | \"down\";\n\nexport function useKeyboardActions(opts: {\n  leaves: LeafRect[];\n  focusedAreaId: string | null;\n  cap: number;\n  dispatch: React.Dispatch<Action>;\n  makeAreaId: () => string;\n}) {\n  const { leaves, focusedAreaId, cap, dispatch, makeAreaId } = opts;\n\n  const focusedLeaf = focusedAreaId\n    ? (leaves.find((l) => l.areaId === focusedAreaId) ?? null)\n    : null;\n  const focusedCanSplit = focusedLeaf !== null && focusedLeaf.depth < cap;\n\n  const splitFocused = useCallback(\n    (orientation: SplitOrientation) => {\n      if (!focusedLeaf) return;\n      if (focusedLeaf.depth >= cap) return;\n      dispatch({\n        type: \"split\",\n        areaId: focusedLeaf.areaId,\n        orientation,\n        newAreaId: makeAreaId(),\n      });\n    },\n    [focusedLeaf, cap, dispatch, makeAreaId],\n  );\n\n  const findNeighbor = useCallback(\n    (areaId: string, direction: ResizeDirection): LeafRect | null => {\n      const origin = leaves.find((l) => l.areaId === areaId);\n      if (!origin) return null;\n      const candidates = leaves.filter((l) => l.areaId !== areaId);\n      switch (direction) {\n        case \"right\":\n          return (\n            candidates.find(\n              (l) =>\n                Math.abs(l.x - (origin.x + origin.width)) < 0.5 &&\n                rectsShareFullEdge(origin, l) === \"vertical\",\n            ) ?? null\n          );\n        case \"left\":\n          return (\n            candidates.find(\n              (l) =>\n                Math.abs(origin.x - (l.x + l.width)) < 0.5 &&\n                rectsShareFullEdge(origin, l) === \"vertical\",\n            ) ?? null\n          );\n        case \"down\":\n          return (\n            candidates.find(\n              (l) =>\n                Math.abs(l.y - (origin.y + origin.height)) < 0.5 &&\n                rectsShareFullEdge(origin, l) === \"horizontal\",\n            ) ?? null\n          );\n        case \"up\":\n          return (\n            candidates.find(\n              (l) =>\n                Math.abs(origin.y - (l.y + l.height)) < 0.5 &&\n                rectsShareFullEdge(origin, l) === \"horizontal\",\n            ) ?? null\n          );\n      }\n    },\n    [leaves],\n  );\n\n  const mergeWithNeighbor = useCallback(\n    (direction: ResizeDirection) => {\n      if (!focusedAreaId) return;\n      const neighbor = findNeighbor(focusedAreaId, direction);\n      if (!neighbor) return;\n      dispatch({\n        type: \"merge\",\n        survivorId: focusedAreaId,\n        absorbedId: neighbor.areaId,\n      });\n    },\n    [focusedAreaId, findNeighbor, dispatch],\n  );\n\n  const swapToComponent = useCallback(\n    (componentId: string) => {\n      if (!focusedAreaId) return;\n      dispatch({ type: \"swap\", areaId: focusedAreaId, componentId });\n    },\n    [focusedAreaId, dispatch],\n  );\n\n  return {\n    splitFocused,\n    mergeWithNeighbor,\n    swapToComponent,\n    findNeighbor,\n    canSplit: focusedCanSplit,\n    KEYBOARD_RESIZE_STEP,\n  };\n}\n\n/**\n * @internal\n *\n * Wires the global `Alt+Shift+Arrow` divider-resize gesture. Not part of the\n * workspace package's stable public API — the only stable exports are those\n * re-exported from `index.ts` (`SplitWorkspace`, `useAreaContext`, the type set).\n * This hook exists solely to keep `workspace.tsx` lean (extracted in v0.1.2\n * patch A-3); a consumer driving Alt+Shift+Arrow on a custom layout would\n * need to supply `leaves` / `dividers` / `renderedTree` themselves, which is\n * not a documented contract and may change in any patch.\n */\nexport function useResizeKeyboard(opts: {\n  enabled: boolean;\n  leaves: LeafRect[];\n  dividers: SplitDividerRect[];\n  renderedTree: AreaTree;\n  focusedAreaId: string | null;\n  dispatch: React.Dispatch<Action>;\n}) {\n  const { enabled, leaves, dividers, renderedTree, focusedAreaId, dispatch } =\n    opts;\n\n  useEffect(() => {\n    if (!enabled) return;\n    const handler = (e: KeyboardEvent) => {\n      if (!focusedAreaId) return;\n      if (!e.altKey || !e.shiftKey) return;\n      const isHorizontal = e.key === \"ArrowLeft\" || e.key === \"ArrowRight\";\n      const isVertical = e.key === \"ArrowUp\" || e.key === \"ArrowDown\";\n      if (!isHorizontal && !isVertical) return;\n      e.preventDefault();\n      const direction =\n        e.key === \"ArrowRight\"\n          ? 1\n          : e.key === \"ArrowLeft\"\n            ? -1\n            : e.key === \"ArrowDown\"\n              ? 1\n              : -1;\n      const orientation = isHorizontal ? \"vertical\" : \"horizontal\";\n      const focusedRect = leaves.find((l) => l.areaId === focusedAreaId);\n      if (!focusedRect) return;\n      const candidate = dividers.find((d) => {\n        if (d.orientation !== orientation) return false;\n        if (orientation === \"vertical\") {\n          const onRight = Math.abs(focusedRect.x + focusedRect.width - d.x) < 1;\n          const onLeft = Math.abs(focusedRect.x - d.x) < 1;\n          return onLeft || onRight;\n        } else {\n          const onBottom =\n            Math.abs(focusedRect.y + focusedRect.height - d.y) < 1;\n          const onTop = Math.abs(focusedRect.y - d.y) < 1;\n          return onTop || onBottom;\n        }\n      });\n      if (!candidate) return;\n      const node = getNodeAtPath(renderedTree, candidate.splitPath);\n      if (node.kind !== \"split\") return;\n      const next = Math.max(\n        0.05,\n        Math.min(0.95, node.ratio + KEYBOARD_RESIZE_STEP * direction),\n      );\n      dispatch({ type: \"resize\", splitPath: candidate.splitPath, ratio: next });\n    };\n    window.addEventListener(\"keydown\", handler);\n    return () => window.removeEventListener(\"keydown\", handler);\n  }, [enabled, focusedAreaId, leaves, dividers, renderedTree, dispatch]);\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/hooks/use-keyboard-actions.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/lib/geometry.ts",
      "content": "import type { SplitOrientation } from \"../types\";\n\nexport type Rect = {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n};\n\nexport type LeafRect = Rect & {\n  areaId: string;\n  componentId: string;\n  depth: number;\n};\n\nexport type SplitDividerRect = {\n  splitPath: number[];\n  orientation: SplitOrientation;\n  x: number;\n  y: number;\n  length: number;\n};\n\nexport function inferDragOrientation(\n  startX: number,\n  startY: number,\n  currentX: number,\n  currentY: number,\n): SplitOrientation {\n  const dx = Math.abs(currentX - startX);\n  const dy = Math.abs(currentY - startY);\n  return dx >= dy ? \"vertical\" : \"horizontal\";\n}\n\nexport function pointInRect(px: number, py: number, rect: Rect): boolean {\n  return (\n    px >= rect.x &&\n    px <= rect.x + rect.width &&\n    py >= rect.y &&\n    py <= rect.y + rect.height\n  );\n}\n\nexport function rectsShareFullEdge(a: Rect, b: Rect): SplitOrientation | null {\n  const eps = 0.5;\n  const aRight = a.x + a.width;\n  const aBottom = a.y + a.height;\n  const bRight = b.x + b.width;\n  const bBottom = b.y + b.height;\n\n  if (Math.abs(aRight - b.x) < eps || Math.abs(bRight - a.x) < eps) {\n    if (Math.abs(a.y - b.y) < eps && Math.abs(aBottom - bBottom) < eps) {\n      return \"vertical\";\n    }\n  }\n  if (Math.abs(aBottom - b.y) < eps || Math.abs(bBottom - a.y) < eps) {\n    if (Math.abs(a.x - b.x) < eps && Math.abs(aRight - bRight) < eps) {\n      return \"horizontal\";\n    }\n  }\n  return null;\n}\n\nexport function clampRatio(\n  ratio: number,\n  totalSize: number,\n  minA: number,\n  minB: number,\n): number {\n  if (totalSize <= minA + minB) return 0.5;\n  const minRatio = minA / totalSize;\n  const maxRatio = (totalSize - minB) / totalSize;\n  return Math.max(minRatio, Math.min(maxRatio, ratio));\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/lib/geometry.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/lib/ids.ts",
      "content": "let counter = 0;\n\nexport function makeAreaId(): string {\n  counter += 1;\n  return `area-${Date.now().toString(36)}-${counter.toString(36)}`;\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/lib/ids.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/lib/reducer.ts",
      "content": "import type { AreaTree, SplitOrientation } from \"../types\";\nimport {\n  mergeAreas,\n  resizeSplit,\n  splitLeaf,\n  swapComponent,\n} from \"./tree\";\n\nexport type SplitWorkspaceState = {\n  tree: AreaTree;\n  focusedAreaId: string | null;\n};\n\nexport type Action =\n  | {\n      type: \"split\";\n      areaId: string;\n      orientation: SplitOrientation;\n      newAreaId: string;\n    }\n  | { type: \"merge\"; survivorId: string; absorbedId: string }\n  | { type: \"resize\"; splitPath: number[]; ratio: number }\n  | { type: \"swap\"; areaId: string; componentId: string }\n  | { type: \"focus\"; areaId: string | null }\n  | { type: \"replace-tree\"; tree: AreaTree };\n\nexport function reducer(state: SplitWorkspaceState, action: Action): SplitWorkspaceState {\n  switch (action.type) {\n    case \"split\": {\n      const tree = splitLeaf(\n        state.tree,\n        action.areaId,\n        action.orientation,\n        action.newAreaId,\n      );\n      return tree === state.tree ? state : { ...state, tree };\n    }\n    case \"merge\": {\n      const tree = mergeAreas(state.tree, action.survivorId, action.absorbedId);\n      if (tree === state.tree) return state;\n      const focusedAreaId =\n        state.focusedAreaId === action.absorbedId\n          ? action.survivorId\n          : state.focusedAreaId;\n      return { tree, focusedAreaId };\n    }\n    case \"resize\": {\n      const tree = resizeSplit(state.tree, action.splitPath, action.ratio);\n      return tree === state.tree ? state : { ...state, tree };\n    }\n    case \"swap\": {\n      const tree = swapComponent(state.tree, action.areaId, action.componentId);\n      return tree === state.tree ? state : { ...state, tree };\n    }\n    case \"focus\": {\n      if (state.focusedAreaId === action.areaId) return state;\n      return { ...state, focusedAreaId: action.areaId };\n    }\n    case \"replace-tree\": {\n      if (action.tree === state.tree) return state;\n      return { ...state, tree: action.tree };\n    }\n  }\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/lib/reducer.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/lib/tree.ts",
      "content": "import type {\n  AreaTree,\n  AreaTreeLeaf,\n  AreaTreeSplit,\n  SplitOrientation,\n} from \"../types\";\nimport { clampRatio } from \"./geometry\";\nimport type { LeafRect, Rect, SplitDividerRect } from \"./geometry\";\n\nexport function findLeaf(\n  tree: AreaTree,\n  areaId: string,\n  path: number[] = [],\n): { node: AreaTreeLeaf; path: number[] } | null {\n  if (tree.kind === \"leaf\") {\n    return tree.id === areaId ? { node: tree, path } : null;\n  }\n  return (\n    findLeaf(tree.a, areaId, [...path, 0]) ??\n    findLeaf(tree.b, areaId, [...path, 1])\n  );\n}\n\nexport function getNodeAtPath(tree: AreaTree, path: number[]): AreaTree {\n  let node: AreaTree = tree;\n  for (const step of path) {\n    if (node.kind !== \"split\") {\n      throw new Error(`Invalid path: encountered leaf before path end`);\n    }\n    node = step === 0 ? node.a : node.b;\n  }\n  return node;\n}\n\nexport function replaceNodeAtPath(\n  tree: AreaTree,\n  path: number[],\n  next: AreaTree,\n): AreaTree {\n  if (path.length === 0) return next;\n  if (tree.kind !== \"split\") {\n    throw new Error(`Cannot descend into a leaf`);\n  }\n  const [head, ...rest] = path;\n  if (head === 0) {\n    return { ...tree, a: replaceNodeAtPath(tree.a, rest, next) };\n  }\n  return { ...tree, b: replaceNodeAtPath(tree.b, rest, next) };\n}\n\nexport function splitLeaf(\n  tree: AreaTree,\n  areaId: string,\n  orientation: SplitOrientation,\n  newAreaId: string,\n): AreaTree {\n  const found = findLeaf(tree, areaId);\n  if (!found) return tree;\n  const { node, path } = found;\n  const split: AreaTreeSplit = {\n    kind: \"split\",\n    orientation,\n    ratio: 0.5,\n    a: node,\n    b: { kind: \"leaf\", id: newAreaId, componentId: node.componentId },\n  };\n  return replaceNodeAtPath(tree, path, split);\n}\n\nexport function mergeAreas(\n  tree: AreaTree,\n  survivorId: string,\n  absorbedId: string,\n): AreaTree {\n  const survivor = findLeaf(tree, survivorId);\n  const absorbed = findLeaf(tree, absorbedId);\n  if (!survivor || !absorbed) return tree;\n  if (survivor.path.length === 0 || absorbed.path.length === 0) return tree;\n  const survivorParent = survivor.path.slice(0, -1);\n  const absorbedParent = absorbed.path.slice(0, -1);\n  if (\n    survivorParent.length !== absorbedParent.length ||\n    !survivorParent.every((step, i) => step === absorbedParent[i])\n  ) {\n    return tree;\n  }\n  return replaceNodeAtPath(tree, survivorParent, survivor.node);\n}\n\nexport function resizeSplit(\n  tree: AreaTree,\n  splitPath: number[],\n  ratio: number,\n): AreaTree {\n  const node = getNodeAtPath(tree, splitPath);\n  if (node.kind !== \"split\") return tree;\n  return replaceNodeAtPath(tree, splitPath, { ...node, ratio });\n}\n\nexport function swapComponent(\n  tree: AreaTree,\n  areaId: string,\n  componentId: string,\n): AreaTree {\n  const found = findLeaf(tree, areaId);\n  if (!found) return tree;\n  return replaceNodeAtPath(tree, found.path, {\n    ...found.node,\n    componentId,\n  });\n}\n\nexport function treeDepth(tree: AreaTree): number {\n  if (tree.kind === \"leaf\") return 0;\n  return 1 + Math.max(treeDepth(tree.a), treeDepth(tree.b));\n}\n\nexport function flattenLeavesInOrder(tree: AreaTree): AreaTreeLeaf[] {\n  if (tree.kind === \"leaf\") return [tree];\n  return [...flattenLeavesInOrder(tree.a), ...flattenLeavesInOrder(tree.b)];\n}\n\nexport function flattenSubtreesPastDepth(\n  tree: AreaTree,\n  cap: number,\n  currentDepth = 0,\n): AreaTree {\n  if (tree.kind === \"leaf\") return tree;\n  if (currentDepth >= cap) {\n    // v0.1: this used `collapseToFirstLeaf`, which silently dropped every\n    // leaf except the leftmost — the contract said \"deeper subtrees flatten\n    // to card stacks within their parent region\" but the implementation lost\n    // content. v0.1.1 (post-review F-01): collapse to a balanced binary chain\n    // of splits over ALL leaves, inheriting the over-depth split's\n    // orientation so the row still feels like the user's original layout\n    // intent. Trade-off: the chain itself has depth ~log2(N) below the cap,\n    // so visual depth is no longer strictly capped — but no leaves are lost.\n    // v0.2 will add a `stack` kind to AreaTree that tiles uniformly without\n    // nested splits, fully honoring the cap; this v0.1.1 fix is a stopgap.\n    return collapseToBalancedSplits(tree, tree.orientation);\n  }\n  return {\n    ...tree,\n    a: flattenSubtreesPastDepth(tree.a, cap, currentDepth + 1),\n    b: flattenSubtreesPastDepth(tree.b, cap, currentDepth + 1),\n  };\n}\n\nfunction collapseToBalancedSplits(\n  tree: AreaTree,\n  orientation: SplitOrientation,\n): AreaTree {\n  if (tree.kind === \"leaf\") return tree;\n  const leaves = flattenLeavesInOrder(tree);\n  return buildBalancedSplits(leaves, orientation);\n}\n\nfunction buildBalancedSplits(\n  leaves: AreaTreeLeaf[],\n  orientation: SplitOrientation,\n): AreaTree {\n  if (leaves.length === 1) return leaves[0];\n  const mid = Math.floor(leaves.length / 2);\n  return {\n    kind: \"split\",\n    orientation,\n    ratio: mid / leaves.length,\n    a: buildBalancedSplits(leaves.slice(0, mid), orientation),\n    b: buildBalancedSplits(leaves.slice(mid), orientation),\n  };\n}\n\nexport function validateTree(\n  tree: AreaTree,\n  registeredComponentIds: string[],\n): { valid: boolean; errors: string[] } {\n  const errors: string[] = [];\n  const seenIds = new Set<string>();\n  const registered = new Set(registeredComponentIds);\n\n  function walk(node: AreaTree) {\n    if (node.kind === \"leaf\") {\n      if (seenIds.has(node.id)) {\n        errors.push(`Duplicate area id: \"${node.id}\"`);\n      }\n      seenIds.add(node.id);\n      if (!registered.has(node.componentId)) {\n        errors.push(\n          `Leaf \"${node.id}\" references unregistered component \"${node.componentId}\"`,\n        );\n      }\n    } else {\n      if (node.ratio <= 0 || node.ratio >= 1) {\n        errors.push(`Split ratio ${node.ratio} out of range (0, 1)`);\n      }\n      walk(node.a);\n      walk(node.b);\n    }\n  }\n\n  walk(tree);\n  return { valid: errors.length === 0, errors };\n}\n\ntype LayoutResult = {\n  leaves: LeafRect[];\n  dividers: SplitDividerRect[];\n};\n\nexport function computeLayout(\n  tree: AreaTree,\n  bounds: Rect,\n  options?: { minSize?: { width: number; height: number } },\n): LayoutResult {\n  const minW = options?.minSize?.width ?? 0;\n  const minH = options?.minSize?.height ?? 0;\n  const leaves: LeafRect[] = [];\n  const dividers: SplitDividerRect[] = [];\n\n  function walk(node: AreaTree, rect: Rect, depth: number, path: number[]) {\n    if (node.kind === \"leaf\") {\n      leaves.push({\n        areaId: node.id,\n        componentId: node.componentId,\n        depth,\n        ...rect,\n      });\n      return;\n    }\n    if (node.orientation === \"vertical\") {\n      const total = rect.width;\n      const r = clampRatio(node.ratio, total, minW, minW);\n      const aWidth = total * r;\n      const bWidth = total - aWidth;\n      walk(\n        node.a,\n        { x: rect.x, y: rect.y, width: aWidth, height: rect.height },\n        depth + 1,\n        [...path, 0],\n      );\n      walk(\n        node.b,\n        {\n          x: rect.x + aWidth,\n          y: rect.y,\n          width: bWidth,\n          height: rect.height,\n        },\n        depth + 1,\n        [...path, 1],\n      );\n      dividers.push({\n        splitPath: path,\n        orientation: \"vertical\",\n        x: rect.x + aWidth,\n        y: rect.y,\n        length: rect.height,\n      });\n    } else {\n      const total = rect.height;\n      const r = clampRatio(node.ratio, total, minH, minH);\n      const aHeight = total * r;\n      const bHeight = total - aHeight;\n      walk(\n        node.a,\n        { x: rect.x, y: rect.y, width: rect.width, height: aHeight },\n        depth + 1,\n        [...path, 0],\n      );\n      walk(\n        node.b,\n        {\n          x: rect.x,\n          y: rect.y + aHeight,\n          width: rect.width,\n          height: bHeight,\n        },\n        depth + 1,\n        [...path, 1],\n      );\n      dividers.push({\n        splitPath: path,\n        orientation: \"horizontal\",\n        x: rect.x,\n        y: rect.y + aHeight,\n        length: rect.width,\n      });\n    }\n  }\n\n  walk(tree, bounds, 0, []);\n  return { leaves, dividers };\n}\n\nexport function computeLayoutBoundsForPath(\n  tree: AreaTree,\n  splitPath: number[],\n  canvasSize: { width: number; height: number },\n  minAreaSize: { width: number; height: number },\n): Rect | null {\n  let x = 0;\n  let y = 0;\n  let width = canvasSize.width;\n  let height = canvasSize.height;\n  let node: AreaTree = tree;\n  for (const step of splitPath) {\n    if (node.kind !== \"split\") return null;\n    const total = node.orientation === \"vertical\" ? width : height;\n    const min =\n      node.orientation === \"vertical\" ? minAreaSize.width : minAreaSize.height;\n    const r = clampRatio(node.ratio, total, min, min);\n    const aSize = total * r;\n    if (node.orientation === \"vertical\") {\n      if (step === 0) {\n        width = aSize;\n      } else {\n        x = x + aSize;\n        width = total - aSize;\n      }\n    } else {\n      if (step === 0) {\n        height = aSize;\n      } else {\n        y = y + aSize;\n        height = total - aSize;\n      }\n    }\n    node = step === 0 ? node.a : node.b;\n  }\n  return { x, y, width, height };\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/lib/tree.ts"
    },
    {
      "path": "src/registry/components/layout/split-workspace/parts/area-body.tsx",
      "content": "\"use client\";\n\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/utils\";\nimport { AreaContextContext } from \"../hooks/use-area-context\";\nimport type { AreaContext } from \"../types\";\n\nexport function AreaBody({\n  ctx,\n  children,\n  className,\n}: {\n  ctx: AreaContext;\n  children: React.ReactNode;\n  className?: string;\n}) {\n  return (\n    <AreaContextContext.Provider value={ctx}>\n      <ScrollArea className={cn(\"h-full w-full\", className)}>\n        <div className=\"h-full w-full\">{children}</div>\n      </ScrollArea>\n    </AreaContextContext.Provider>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/parts/area-body.tsx"
    },
    {
      "path": "src/registry/components/layout/split-workspace/parts/area-header.tsx",
      "content": "\"use client\";\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { ChevronsUpDownIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { ComponentPicker } from \"./component-picker\";\nimport type { SplitWorkspaceComponent } from \"../types\";\n\nexport function AreaHeader({\n  components,\n  currentId,\n  onSelectComponent,\n  onSplitVertical,\n  onSplitHorizontal,\n  onMergeDirection,\n  canSplit,\n  mergeOptions,\n  className,\n}: {\n  components: SplitWorkspaceComponent[];\n  currentId: string;\n  onSelectComponent: (componentId: string) => void;\n  onSplitVertical: () => void;\n  onSplitHorizontal: () => void;\n  onMergeDirection: (direction: \"left\" | \"right\" | \"up\" | \"down\") => void;\n  canSplit: boolean;\n  mergeOptions: { left: boolean; right: boolean; up: boolean; down: boolean };\n  className?: string;\n}) {\n  return (\n    <div\n      className={cn(\n        \"flex h-7 items-center justify-between border-b border-border bg-card px-2\",\n        className,\n      )}\n    >\n      <ComponentPicker\n        components={components}\n        currentId={currentId}\n        onSelect={onSelectComponent}\n      />\n      <DropdownMenu>\n        <DropdownMenuTrigger\n          aria-label=\"Area actions\"\n          className=\"inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        >\n          <ChevronsUpDownIcon className=\"size-3\" aria-hidden />\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"end\" className=\"min-w-44\">\n          <DropdownMenuLabel className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">\n            Split\n          </DropdownMenuLabel>\n          <DropdownMenuItem\n            disabled={!canSplit}\n            onSelect={onSplitVertical}\n            className=\"text-xs\"\n          >\n            Split vertical\n          </DropdownMenuItem>\n          <DropdownMenuItem\n            disabled={!canSplit}\n            onSelect={onSplitHorizontal}\n            className=\"text-xs\"\n          >\n            Split horizontal\n          </DropdownMenuItem>\n          <DropdownMenuSeparator />\n          <DropdownMenuSub>\n            <DropdownMenuSubTrigger\n              disabled={\n                !mergeOptions.left &&\n                !mergeOptions.right &&\n                !mergeOptions.up &&\n                !mergeOptions.down\n              }\n              className=\"text-xs\"\n            >\n              Merge with neighbor\n            </DropdownMenuSubTrigger>\n            <DropdownMenuSubContent className=\"min-w-32\">\n              <DropdownMenuItem\n                disabled={!mergeOptions.up}\n                onSelect={() => onMergeDirection(\"up\")}\n                className=\"text-xs\"\n              >\n                Up\n              </DropdownMenuItem>\n              <DropdownMenuItem\n                disabled={!mergeOptions.down}\n                onSelect={() => onMergeDirection(\"down\")}\n                className=\"text-xs\"\n              >\n                Down\n              </DropdownMenuItem>\n              <DropdownMenuItem\n                disabled={!mergeOptions.left}\n                onSelect={() => onMergeDirection(\"left\")}\n                className=\"text-xs\"\n              >\n                Left\n              </DropdownMenuItem>\n              <DropdownMenuItem\n                disabled={!mergeOptions.right}\n                onSelect={() => onMergeDirection(\"right\")}\n                className=\"text-xs\"\n              >\n                Right\n              </DropdownMenuItem>\n            </DropdownMenuSubContent>\n          </DropdownMenuSub>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/parts/area-header.tsx"
    },
    {
      "path": "src/registry/components/layout/split-workspace/parts/area.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { AreaBody } from \"./area-body\";\nimport { AreaHeader } from \"./area-header\";\nimport { CornerHandle } from \"./corner-handle\";\nimport type { AreaContext, SplitWorkspaceComponent } from \"../types\";\n\nexport function Area({\n  areaId,\n  componentId,\n  components,\n  width,\n  height,\n  isFocused,\n  cornerInert,\n  canSplit,\n  mergeOptions,\n  onCornerPointerDown,\n  onSelectComponent,\n  onSplitVertical,\n  onSplitHorizontal,\n  onMergeDirection,\n  onAreaPointerDown,\n}: {\n  areaId: string;\n  componentId: string;\n  components: SplitWorkspaceComponent[];\n  width: number;\n  height: number;\n  isFocused: boolean;\n  cornerInert: boolean;\n  canSplit: boolean;\n  mergeOptions: { left: boolean; right: boolean; up: boolean; down: boolean };\n  onCornerPointerDown: (\n    areaId: string,\n    e: React.PointerEvent<HTMLDivElement>,\n  ) => void;\n  onSelectComponent: (componentId: string) => void;\n  onSplitVertical: () => void;\n  onSplitHorizontal: () => void;\n  onMergeDirection: (direction: \"left\" | \"right\" | \"up\" | \"down\") => void;\n  onAreaPointerDown: () => void;\n}) {\n  const component = components.find((c) => c.id === componentId);\n  const headerHeight = 28;\n\n  const ctx = useMemo<AreaContext>(\n    () => ({\n      areaId,\n      width,\n      height: Math.max(0, height - headerHeight),\n      isFocused,\n    }),\n    [areaId, width, height, isFocused, headerHeight],\n  );\n\n  const handleCornerDown = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      e.stopPropagation();\n      onCornerPointerDown(areaId, e);\n    },\n    [areaId, onCornerPointerDown],\n  );\n\n  return (\n    <div\n      data-area-id={areaId}\n      tabIndex={0}\n      role=\"region\"\n      aria-label={component?.name ?? \"Area\"}\n      onPointerDownCapture={onAreaPointerDown}\n      className={cn(\n        \"absolute flex flex-col overflow-hidden rounded-md border border-border bg-card\",\n        isFocused && \"ring-1 ring-primary/40\",\n      )}\n      style={{ width: \"100%\", height: \"100%\" }}\n    >\n      <AreaHeader\n        components={components}\n        currentId={componentId}\n        onSelectComponent={onSelectComponent}\n        onSplitVertical={onSplitVertical}\n        onSplitHorizontal={onSplitHorizontal}\n        onMergeDirection={onMergeDirection}\n        canSplit={canSplit}\n        mergeOptions={mergeOptions}\n      />\n      <div className=\"relative flex-1 overflow-hidden\">\n        <AreaBody ctx={ctx}>\n          {component ? (\n            component.render()\n          ) : (\n            <div className=\"flex h-full w-full items-center justify-center p-6 text-center\">\n              <div>\n                <p className=\"text-sm font-medium text-foreground\">\n                  Component &quot;{componentId}&quot; not registered\n                </p>\n                <p className=\"mt-1 text-xs text-muted-foreground\">\n                  Use the dropdown to pick one from the registry.\n                </p>\n              </div>\n            </div>\n          )}\n        </AreaBody>\n      </div>\n      <CornerHandle position=\"tl\" disabled={cornerInert} onPointerDown={handleCornerDown} />\n      <CornerHandle position=\"tr\" disabled={cornerInert} onPointerDown={handleCornerDown} />\n      <CornerHandle position=\"bl\" disabled={cornerInert} onPointerDown={handleCornerDown} />\n      <CornerHandle position=\"br\" disabled={cornerInert} onPointerDown={handleCornerDown} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/parts/area.tsx"
    },
    {
      "path": "src/registry/components/layout/split-workspace/parts/card-stack.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { AreaContextContext } from \"../hooks/use-area-context\";\nimport { ComponentPicker } from \"./component-picker\";\nimport type { AreaTreeLeaf, SplitWorkspaceComponent } from \"../types\";\n\nconst DEFAULT_STACK_CARD_HEIGHT = 320;\n\nexport function CardStack({\n  leaves,\n  components,\n  onSelectComponent,\n  itemHeight = DEFAULT_STACK_CARD_HEIGHT,\n}: {\n  leaves: AreaTreeLeaf[];\n  components: SplitWorkspaceComponent[];\n  onSelectComponent: (areaId: string, componentId: string) => void;\n  itemHeight?: number;\n}) {\n  return (\n    <ScrollArea className=\"h-full w-full\">\n      <div className=\"flex flex-col gap-3 p-3\">\n        {leaves.map((leaf) => (\n          <StackedCard\n            key={leaf.id}\n            leaf={leaf}\n            components={components}\n            itemHeight={itemHeight}\n            onSelectComponent={(componentId) =>\n              onSelectComponent(leaf.id, componentId)\n            }\n          />\n        ))}\n      </div>\n    </ScrollArea>\n  );\n}\n\nfunction StackedCard({\n  leaf,\n  components,\n  itemHeight,\n  onSelectComponent,\n}: {\n  leaf: AreaTreeLeaf;\n  components: SplitWorkspaceComponent[];\n  itemHeight: number;\n  onSelectComponent: (componentId: string) => void;\n}) {\n  const component = components.find((c) => c.id === leaf.componentId);\n  const ctx = useMemo(\n    () => ({\n      areaId: leaf.id,\n      width: 0,\n      height: itemHeight,\n      isFocused: false,\n    }),\n    [leaf.id, itemHeight],\n  );\n  return (\n    <div\n      data-area-id={leaf.id}\n      role=\"region\"\n      aria-label={component?.name ?? \"Area\"}\n      className=\"overflow-hidden rounded-md border border-border bg-card\"\n    >\n      <div className=\"flex h-7 items-center justify-between border-b border-border bg-card px-2\">\n        <ComponentPicker\n          components={components}\n          currentId={leaf.componentId}\n          onSelect={onSelectComponent}\n        />\n      </div>\n      <div\n        className=\"relative overflow-hidden\"\n        style={{ height: itemHeight }}\n      >\n        <AreaContextContext.Provider value={ctx}>\n          <ScrollArea className=\"h-full w-full\">\n            <div className=\"h-full w-full\">\n              {component ? (\n                component.render()\n              ) : (\n                <div className=\"flex h-full w-full items-center justify-center p-6 text-center text-xs text-muted-foreground\">\n                  Component &quot;{leaf.componentId}&quot; not registered\n                </div>\n              )}\n            </div>\n          </ScrollArea>\n        </AreaContextContext.Provider>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/parts/card-stack.tsx"
    },
    {
      "path": "src/registry/components/layout/split-workspace/parts/component-picker.tsx",
      "content": "\"use client\";\n\nimport { ChevronDownIcon } from \"lucide-react\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\nimport type { SplitWorkspaceComponent } from \"../types\";\n\ntype Group = {\n  category: string;\n  items: SplitWorkspaceComponent[];\n};\n\nfunction groupByCategory(components: SplitWorkspaceComponent[]): Group[] {\n  const map = new Map<string, SplitWorkspaceComponent[]>();\n  for (const c of components) {\n    const key = c.category ?? \"\";\n    const list = map.get(key) ?? [];\n    list.push(c);\n    map.set(key, list);\n  }\n  return Array.from(map.entries()).map(([category, items]) => ({\n    category,\n    items,\n  }));\n}\n\nexport function ComponentPicker({\n  components,\n  currentId,\n  onSelect,\n  className,\n}: {\n  components: SplitWorkspaceComponent[];\n  currentId: string;\n  onSelect: (componentId: string) => void;\n  className?: string;\n}) {\n  const current = components.find((c) => c.id === currentId);\n  const groups = groupByCategory(components);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        className={cn(\n          \"inline-flex items-center gap-1.5 rounded-sm px-1.5 py-0.5 text-xs font-medium text-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          className,\n        )}\n        aria-label={`Change component (current: ${current?.name ?? currentId})`}\n      >\n        {current?.icon ? (\n          <span className=\"inline-flex size-3 items-center justify-center text-muted-foreground\">\n            {current.icon}\n          </span>\n        ) : null}\n        <span className=\"truncate\">{current?.name ?? \"—\"}</span>\n        <ChevronDownIcon className=\"size-3 text-muted-foreground\" aria-hidden />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\" className=\"min-w-44\">\n        {groups.map((group, gi) => (\n          <DropdownMenuGroup key={group.category || `__${gi}`}>\n            {group.category ? (\n              <DropdownMenuLabel className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">\n                {group.category}\n              </DropdownMenuLabel>\n            ) : null}\n            {group.items.map((item) => (\n              <DropdownMenuItem\n                key={item.id}\n                onSelect={() => onSelect(item.id)}\n                className=\"text-xs\"\n              >\n                {item.icon ? (\n                  <span className=\"mr-2 inline-flex size-3 items-center justify-center text-muted-foreground\">\n                    {item.icon}\n                  </span>\n                ) : null}\n                <span>{item.name}</span>\n              </DropdownMenuItem>\n            ))}\n            {gi < groups.length - 1 ? <DropdownMenuSeparator /> : null}\n          </DropdownMenuGroup>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/parts/component-picker.tsx"
    },
    {
      "path": "src/registry/components/layout/split-workspace/parts/corner-handle.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type CornerPosition = \"tl\" | \"tr\" | \"bl\" | \"br\";\n\nconst POSITION_CLASSES: Record<CornerPosition, string> = {\n  tl: \"top-0 left-0 cursor-crosshair\",\n  tr: \"top-0 right-0 cursor-crosshair\",\n  bl: \"bottom-0 left-0 cursor-crosshair\",\n  br: \"bottom-0 right-0 cursor-crosshair\",\n};\n\nconst PATTERN_CLASSES: Record<CornerPosition, string> = {\n  tl: \"[background:radial-gradient(currentColor_1px,transparent_1px)_0_0/4px_4px] [mask-image:linear-gradient(135deg,black,transparent_60%)]\",\n  tr: \"[background:radial-gradient(currentColor_1px,transparent_1px)_0_0/4px_4px] [mask-image:linear-gradient(225deg,black,transparent_60%)]\",\n  bl: \"[background:radial-gradient(currentColor_1px,transparent_1px)_0_0/4px_4px] [mask-image:linear-gradient(45deg,black,transparent_60%)]\",\n  br: \"[background:radial-gradient(currentColor_1px,transparent_1px)_0_0/4px_4px] [mask-image:linear-gradient(315deg,black,transparent_60%)]\",\n};\n\nexport function CornerHandle({\n  position,\n  disabled,\n  onPointerDown,\n}: {\n  position: CornerPosition;\n  disabled?: boolean;\n  onPointerDown: (e: React.PointerEvent<HTMLDivElement>) => void;\n}) {\n  return (\n    <div\n      // v0.1.4 (review): pointer-only affordance — hidden from AT. It was a\n      // keyboard-unreachable `role=\"button\"` before; the accessible\n      // equivalent (split/merge) lives in the AreaHeader menu.\n      aria-hidden=\"true\"\n      onPointerDown={\n        disabled\n          ? undefined\n          : (e) => {\n              e.preventDefault();\n              onPointerDown(e);\n            }\n      }\n      className={cn(\n        \"absolute z-10 size-3.5 touch-none select-none text-muted-foreground/40 transition-colors hover:text-foreground/70\",\n        POSITION_CLASSES[position],\n        PATTERN_CLASSES[position],\n        disabled && \"cursor-default opacity-30 hover:text-muted-foreground/40\",\n      )}\n      data-corner={position}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/parts/corner-handle.tsx"
    },
    {
      "path": "src/registry/components/layout/split-workspace/parts/drag-overlay.tsx",
      "content": "\"use client\";\n\nimport { ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon } from \"lucide-react\";\nimport type { CornerDragState } from \"../hooks/use-corner-gesture\";\nimport type { LeafRect } from \"../lib/geometry\";\n\nexport function DragOverlay({\n  dragState,\n  leaves,\n}: {\n  dragState: CornerDragState | null;\n  leaves: LeafRect[];\n}) {\n  if (!dragState) return null;\n  const intent = dragState.intent;\n\n  if (intent.kind === \"split\") {\n    const isVertical = intent.orientation === \"vertical\";\n    const origin = leaves.find((l) => l.areaId === dragState.originAreaId);\n    if (!origin) return null;\n    return (\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute z-40 bg-primary/60\"\n        style={\n          isVertical\n            ? {\n                left: intent.position.x - 1,\n                top: origin.y,\n                width: 2,\n                height: origin.height,\n              }\n            : {\n                left: origin.x,\n                top: intent.position.y - 1,\n                width: origin.width,\n                height: 2,\n              }\n        }\n      />\n    );\n  }\n\n  if (intent.kind === \"merge\") {\n    const origin = leaves.find((l) => l.areaId === dragState.originAreaId);\n    const target = leaves.find((l) => l.areaId === intent.targetAreaId);\n    if (!origin || !target) return null;\n    let direction: \"left\" | \"right\" | \"up\" | \"down\" = \"right\";\n    if (target.x > origin.x + origin.width / 2) direction = \"right\";\n    else if (target.x + target.width < origin.x + origin.width / 2)\n      direction = \"left\";\n    else if (target.y > origin.y + origin.height / 2) direction = \"down\";\n    else direction = \"up\";\n\n    const Icon =\n      direction === \"right\"\n        ? ArrowRightIcon\n        : direction === \"left\"\n          ? ArrowLeftIcon\n          : direction === \"down\"\n            ? ArrowDownIcon\n            : ArrowUpIcon;\n\n    return (\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute z-40 flex items-center justify-center rounded-sm bg-destructive/15 ring-2 ring-destructive\"\n        style={{\n          left: target.x,\n          top: target.y,\n          width: target.width,\n          height: target.height,\n        }}\n      >\n        <div className=\"rounded-full bg-destructive p-2 text-destructive-foreground\">\n          <Icon className=\"size-5\" aria-hidden />\n        </div>\n      </div>\n    );\n  }\n\n  return null;\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/parts/drag-overlay.tsx"
    },
    {
      "path": "src/registry/components/layout/split-workspace/parts/presets-tabs.tsx",
      "content": "\"use client\";\n\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport type { SplitWorkspacePreset } from \"../types\";\n\nexport function PresetsTabs({\n  presets,\n  activePresetId,\n  onChange,\n}: {\n  presets: SplitWorkspacePreset[];\n  activePresetId: string | null;\n  onChange: (id: string) => void;\n}) {\n  if (presets.length === 0) return null;\n  return (\n    <div className=\"flex items-center border-b border-border bg-background px-2 py-1.5\">\n      <Tabs\n        value={activePresetId ?? presets[0]?.id}\n        onValueChange={onChange}\n      >\n        <TabsList className=\"h-7\">\n          {presets.map((preset) => (\n            <TabsTrigger\n              key={preset.id}\n              value={preset.id}\n              className=\"h-6 px-2.5 text-xs\"\n            >\n              {preset.name}\n            </TabsTrigger>\n          ))}\n        </TabsList>\n      </Tabs>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/parts/presets-tabs.tsx"
    },
    {
      "path": "src/registry/components/layout/split-workspace/parts/split-divider.tsx",
      "content": "\"use client\";\n\nimport { useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport type { SplitOrientation } from \"../types\";\n\nexport function SplitDivider({\n  orientation,\n  x,\n  y,\n  length,\n  ratioPercent,\n  onPointerDown,\n  onKeyResize,\n}: {\n  orientation: SplitOrientation;\n  x: number;\n  y: number;\n  length: number;\n  ratioPercent: number;\n  onPointerDown: (e: React.PointerEvent<HTMLDivElement>) => void;\n  onKeyResize?: (delta: number) => void;\n}) {\n  const isVertical = orientation === \"vertical\";\n  const ref = useRef<HTMLDivElement>(null);\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    if (!onKeyResize) return;\n    let delta = 0;\n    if (isVertical) {\n      if (e.key === \"ArrowRight\") delta = 1;\n      else if (e.key === \"ArrowLeft\") delta = -1;\n      else return;\n    } else {\n      if (e.key === \"ArrowDown\") delta = 1;\n      else if (e.key === \"ArrowUp\") delta = -1;\n      else return;\n    }\n    e.preventDefault();\n    onKeyResize(delta);\n  };\n  return (\n    <div\n      ref={ref}\n      role=\"separator\"\n      // v0.1.4 (review): keyboard-reachable — the arrow-key resize handler\n      // below was previously dead for keyboard users (tabIndex={-1}).\n      tabIndex={0}\n      aria-label={isVertical ? \"Resize areas (left/right)\" : \"Resize areas (up/down)\"}\n      aria-orientation={isVertical ? \"vertical\" : \"horizontal\"}\n      aria-valuenow={Math.round(ratioPercent)}\n      aria-valuemin={0}\n      aria-valuemax={100}\n      onPointerDown={(e) => {\n        e.preventDefault();\n        ref.current?.focus();\n        onPointerDown(e);\n      }}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"absolute z-20 touch-none select-none bg-transparent transition-colors hover:bg-primary/40 focus-visible:bg-primary/40 focus-visible:outline-none\",\n        isVertical ? \"cursor-col-resize\" : \"cursor-row-resize\",\n      )}\n      style={\n        isVertical\n          ? {\n              left: x - 3,\n              top: y,\n              width: 6,\n              height: length,\n            }\n          : {\n              left: x,\n              top: y - 3,\n              width: length,\n              height: 6,\n            }\n      }\n    >\n      <span\n        aria-hidden\n        className={cn(\n          \"pointer-events-none absolute bg-border\",\n          isVertical\n            ? \"left-1/2 top-0 h-full w-px -translate-x-1/2\"\n            : \"left-0 top-1/2 h-px w-full -translate-y-1/2\",\n        )}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/split-workspace/parts/split-divider.tsx"
    }
  ],
  "categories": [
    "layout"
  ],
  "type": "registry:block"
}