{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "nested-react-splitter",
  "title": "Nested React Splitter",
  "description": "A flexible React splitter layout with an independent sidebar and stacked workspace panels.",
  "dependencies": [],
  "files": [
    {
      "path": "components/splitters/Splitter.tsx",
      "content": "'use client';\n\nimport {\n  useId,\n  useRef,\n  useState,\n  type KeyboardEvent,\n  type PointerEvent,\n  type ReactNode,\n} from 'react';\n\ntype Orientation = 'horizontal' | 'vertical';\n\ntype ResizablePanelsProps = {\n  orientation: Orientation;\n  firstPanel: ReactNode;\n  secondPanel: ReactNode;\n  defaultSize?: number;\n  minSize?: number;\n  ariaLabel?: string;\n  showHandle?: boolean;\n  className?: string;\n};\n\nfunction ResizablePanels({\n  orientation,\n  firstPanel,\n  secondPanel,\n  defaultSize = 50,\n  minSize = 20,\n  ariaLabel = 'Resize panels',\n  showHandle = true,\n  className = '',\n}: ResizablePanelsProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const draggingRef = useRef(false);\n  const [isActive, setIsActive] = useState(false);\n  const firstPanelId = useId();\n  const secondPanelId = useId();\n  const isHorizontal = orientation === 'horizontal';\n  const safeMinSize = Math.min(49, Math.max(0, minSize));\n  const clamp = (value: number) =>\n    Math.min(100 - safeMinSize, Math.max(safeMinSize, value));\n  const [size, setSize] = useState(() => clamp(defaultSize));\n\n  const resizeFromPointer = (clientX: number, clientY: number) => {\n    const bounds = rootRef.current?.getBoundingClientRect();\n    if (!bounds) return;\n\n    const position = isHorizontal ? clientX - bounds.left : clientY - bounds.top;\n    const total = isHorizontal ? bounds.width : bounds.height;\n    setSize(clamp((position / total) * 100));\n  };\n\n  const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {\n    draggingRef.current = true;\n    setIsActive(true);\n    event.currentTarget.setPointerCapture(event.pointerId);\n  };\n\n  const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {\n    if (draggingRef.current) resizeFromPointer(event.clientX, event.clientY);\n  };\n\n  const handlePointerUp = (event: PointerEvent<HTMLDivElement>) => {\n    draggingRef.current = false;\n    setIsActive(false);\n    if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n      event.currentTarget.releasePointerCapture(event.pointerId);\n    }\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    const decreaseKey = isHorizontal ? 'ArrowLeft' : 'ArrowUp';\n    const increaseKey = isHorizontal ? 'ArrowRight' : 'ArrowDown';\n    const step = event.shiftKey ? 10 : 2;\n\n    if (event.key === decreaseKey || event.key === increaseKey) {\n      event.preventDefault();\n      setSize((value) =>\n        clamp(value + (event.key === increaseKey ? step : -step)),\n      );\n    } else if (event.key === 'Home') {\n      event.preventDefault();\n      setSize(safeMinSize);\n    } else if (event.key === 'End') {\n      event.preventDefault();\n      setSize(100 - safeMinSize);\n    }\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      className={[\n        'relative flex overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm',\n        isHorizontal ? 'flex-row' : 'flex-col',\n        className,\n      ]\n        .filter(Boolean)\n        .join(' ')}\n    >\n      <div\n        id={firstPanelId}\n        className='min-h-0 min-w-0 shrink-0 overflow-auto'\n        style={{ flexBasis: `${size}%` }}\n      >\n        {firstPanel}\n      </div>\n\n      <div\n        role='separator'\n        tabIndex={0}\n        aria-label={ariaLabel}\n        aria-controls={`${firstPanelId} ${secondPanelId}`}\n        aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}\n        aria-valuemin={safeMinSize}\n        aria-valuemax={100 - safeMinSize}\n        aria-valuenow={Math.round(size)}\n        onDoubleClick={() => setSize(clamp(defaultSize))}\n        onKeyDown={handleKeyDown}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerUp}\n        onLostPointerCapture={() => {\n          draggingRef.current = false;\n          setIsActive(false);\n        }}\n        className={[\n          'absolute z-10 touch-none bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-gray-300',\n          isHorizontal\n            ? 'bottom-0 top-0 w-3 -translate-x-1/2 cursor-col-resize'\n            : 'left-0 right-0 h-3 -translate-y-1/2 cursor-row-resize',\n        ].join(' ')}\n        style={isHorizontal ? { left: `${size}%` } : { top: `${size}%` }}\n      >\n        <span\n          aria-hidden='true'\n          className={[\n            isActive ? 'absolute bg-indigo-400' : 'absolute bg-gray-200',\n            isHorizontal\n              ? 'bottom-0 left-1/2 top-0 w-px -translate-x-1/2'\n              : 'left-0 right-0 top-1/2 h-px -translate-y-1/2',\n          ].join(' ')}\n        />\n        {showHandle ? (\n          <span\n            aria-hidden='true'\n            className={[\n              'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-gray-200 bg-white',\n              isHorizontal ? 'h-9 w-1.5' : 'h-1.5 w-9',\n            ].join(' ')}\n          />\n        ) : null}\n      </div>\n\n      <div\n        id={secondPanelId}\n        className='min-h-0 min-w-0 shrink-0 overflow-auto'\n        style={{ flexBasis: `${100 - size}%` }}\n      >\n        {secondPanel}\n      </div>\n    </div>\n  );\n}\n\nfunction Placeholder({ label }: { label: string }) {\n  return (\n    <div className='flex h-full min-h-32 items-center justify-center bg-white p-6 text-sm font-medium text-gray-500'>\n      {label}\n    </div>\n  );\n}\n\nexport type HorizontalSplitterProps = {\n  leftPanel?: ReactNode;\n  rightPanel?: ReactNode;\n  defaultSize?: number;\n  minSize?: number;\n  ariaLabel?: string;\n  showHandle?: boolean;\n  className?: string;\n};\n\nexport function HorizontalSplitter({\n  leftPanel = <Placeholder label='Left panel' />,\n  rightPanel = <Placeholder label='Right panel' />,\n  defaultSize = 36,\n  minSize = 20,\n  ariaLabel = 'Resize left and right panels',\n  showHandle = true,\n  className = '',\n}: HorizontalSplitterProps) {\n  return (\n    <ResizablePanels\n      orientation='horizontal'\n      firstPanel={leftPanel}\n      secondPanel={rightPanel}\n      defaultSize={defaultSize}\n      minSize={minSize}\n      ariaLabel={ariaLabel}\n      showHandle={showHandle}\n      className={className}\n    />\n  );\n}\n\nexport type VerticalSplitterProps = {\n  topPanel?: ReactNode;\n  bottomPanel?: ReactNode;\n  defaultSize?: number;\n  minSize?: number;\n  ariaLabel?: string;\n  showHandle?: boolean;\n  className?: string;\n};\n\nexport function VerticalSplitter({\n  topPanel = <Placeholder label='Top panel' />,\n  bottomPanel = <Placeholder label='Bottom panel' />,\n  defaultSize = 58,\n  minSize = 20,\n  ariaLabel = 'Resize top and bottom panels',\n  showHandle = true,\n  className = '',\n}: VerticalSplitterProps) {\n  return (\n    <ResizablePanels\n      orientation='vertical'\n      firstPanel={topPanel}\n      secondPanel={bottomPanel}\n      defaultSize={defaultSize}\n      minSize={minSize}\n      ariaLabel={ariaLabel}\n      showHandle={showHandle}\n      className={className}\n    />\n  );\n}\n\nexport type NestedSplitterProps = {\n  sidebar?: ReactNode;\n  topPanel?: ReactNode;\n  bottomPanel?: ReactNode;\n  defaultSidebarSize?: number;\n  defaultTopSize?: number;\n  minSize?: number;\n  showHandle?: boolean;\n  className?: string;\n};\n\nexport function NestedSplitter({\n  sidebar = <Placeholder label='Navigation' />,\n  topPanel = <Placeholder label='Workspace' />,\n  bottomPanel = <Placeholder label='Console' />,\n  defaultSidebarSize = 28,\n  defaultTopSize = 64,\n  minSize = 20,\n  showHandle = true,\n  className = '',\n}: NestedSplitterProps) {\n  return (\n    <HorizontalSplitter\n      leftPanel={sidebar}\n      rightPanel={\n        <VerticalSplitter\n          topPanel={topPanel}\n          bottomPanel={bottomPanel}\n          defaultSize={defaultTopSize}\n          minSize={minSize}\n          showHandle={showHandle}\n          className='h-full w-full rounded-none border-0 shadow-none'\n        />\n      }\n      defaultSize={defaultSidebarSize}\n      minSize={minSize}\n      showHandle={showHandle}\n      className={className}\n    />\n  );\n}\n\nexport default HorizontalSplitter;\n",
      "type": "registry:component",
      "target": "@components/splitters/Splitter.tsx"
    }
  ],
  "categories": [
    "splitters"
  ],
  "type": "registry:component"
}