{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "resizable-panels",
  "title": "Resizable Panels",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://ui.hotfix.jobs/r/tokens.json",
    "https://ui.hotfix.jobs/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/components/ui/resizable-panels.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport type * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype PanelSide = \"left\" | \"right\";\n\ninterface PanelLayout {\n  width: number;\n  min: number;\n  max: number;\n  open: boolean;\n  onResize: (width: number) => void;\n}\n\ninterface ResizablePanelsContextValue {\n  left?: PanelLayout;\n  right?: PanelLayout;\n}\n\nconst ResizablePanelsContext =\n  createContext<ResizablePanelsContextValue | null>(null);\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.min(Math.max(value, min), max);\n}\n\nexport function usePanelWidth(\n  key: string,\n  defaultWidth: number,\n  min: number,\n  max: number,\n): readonly [number, (width: number) => void] {\n  const [width, setWidth] = useState(() => clamp(defaultWidth, min, max));\n  useEffect(() => {\n    const stored = Number.parseFloat(window.localStorage.getItem(key) ?? \"\");\n    const initialWidth = Number.isFinite(stored)\n      ? clamp(stored, min, max)\n      : clamp(defaultWidth, min, max);\n    setWidth(initialWidth);\n    window.localStorage.setItem(key, String(initialWidth));\n  }, [defaultWidth, key, max, min]);\n  const updateWidth = useCallback(\n    (nextWidth: number) => {\n      const clampedWidth = clamp(nextWidth, min, max);\n      setWidth(clampedWidth);\n      window.localStorage.setItem(key, String(clampedWidth));\n    },\n    [key, max, min],\n  );\n\n  return [width, updateWidth] as const;\n}\n\nexport interface SidePanelProps extends useRender.ComponentProps<\"div\"> {\n  side: PanelSide;\n  open: boolean;\n  width: number;\n  min: number;\n  max: number;\n  onResize: (width: number) => void;\n}\n\nexport function SidePanel({\n  side,\n  open: _open,\n  width: _width,\n  min: _min,\n  max: _max,\n  onResize: _onResize,\n  children,\n  className,\n  style,\n  render,\n  ...props\n}: SidePanelProps): React.ReactElement {\n  const layout = useContext(ResizablePanelsContext)?.[side];\n  const width = layout?.width ?? 0;\n  const defaultProps = {\n    \"data-slot\": \"resizable-side-panel-content\",\n    \"aria-hidden\": !layout?.open || undefined,\n    inert: !layout?.open,\n    className: cn(\n      \"absolute inset-y-0\",\n      side === \"left\" ? \"left-0\" : \"right-0\",\n      className,\n    ),\n    style: { ...style, width },\n    children,\n  };\n  const content = useRender({\n    defaultTagName: \"div\",\n    render,\n    props: mergeProps<\"div\">(props, defaultProps),\n  });\n\n  return (\n    <div\n      data-slot=\"resizable-side-panel\"\n      data-side={side}\n      className={cn(\n        \"relative row-start-1 min-w-0 overflow-hidden\",\n        side === \"left\" ? \"col-start-1\" : \"col-start-3\",\n      )}\n    >\n      {content}\n    </div>\n  );\n}\n\nexport interface ResizeHandleProps\n  extends useRender.ComponentProps<\"div\"> {\n  side: PanelSide;\n}\n\nexport function ResizeHandle({\n  side,\n  className,\n  children,\n  render,\n  style,\n  onKeyDown,\n  onPointerDown,\n  ...props\n}: ResizeHandleProps): React.ReactElement | null {\n  const layout = useContext(ResizablePanelsContext)?.[side];\n  const dragCleanupRef = useRef<(() => void) | null>(null);\n\n  useEffect(\n    () => () => {\n      dragCleanupRef.current?.();\n    },\n    [],\n  );\n\n  const panelLayout = layout ?? {\n    width: 0,\n    min: 0,\n    max: 0,\n    open: false,\n    onResize: () => {},\n  };\n\n  const defaultProps = {\n    role: \"separator\",\n    \"aria-label\": `Resize ${side} panel`,\n    \"aria-orientation\": \"vertical\" as const,\n    \"aria-valuenow\": Math.round(panelLayout.width),\n    \"aria-valuemin\": Math.round(panelLayout.min),\n    \"aria-valuemax\": Math.round(panelLayout.max),\n    \"aria-valuetext\": `${Math.round(panelLayout.width)} pixels`,\n    tabIndex: 0,\n    \"data-slot\": \"resize-handle\",\n    \"data-side\": side,\n    className: cn(\n      \"group absolute inset-y-0 z-10 w-2 cursor-col-resize touch-none outline-none pointer-coarse:w-6\",\n      side === \"left\" ? \"-translate-x-1/2\" : \"translate-x-1/2\",\n      className,\n    ),\n    style: {\n      ...style,\n      ...(side === \"left\"\n        ? { left: panelLayout.width, right: undefined }\n        : { left: undefined, right: panelLayout.width }),\n    },\n    onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => {\n      onKeyDown?.(event);\n      if (event.defaultPrevented) return;\n      if (event.key !== \"ArrowLeft\" && event.key !== \"ArrowRight\") return;\n      event.preventDefault();\n      const direction = event.key === \"ArrowRight\" ? 1 : -1;\n      const sideDirection = side === \"left\" ? direction : -direction;\n      panelLayout.onResize(\n        clamp(\n          panelLayout.width + sideDirection * 10,\n          panelLayout.min,\n          panelLayout.max,\n        ),\n      );\n    },\n    onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => {\n      onPointerDown?.(event);\n      if (\n        event.defaultPrevented ||\n        (event.button != null && event.button !== 0)\n      )\n        return;\n      event.preventDefault();\n      event.currentTarget.setPointerCapture?.(event.pointerId);\n      const startX = event.clientX;\n      const startWidth = panelLayout.width;\n      const move = (moveEvent: PointerEvent) => {\n        const delta = moveEvent.clientX - startX;\n        const sideDelta = side === \"left\" ? delta : -delta;\n        panelLayout.onResize(\n          clamp(startWidth + sideDelta, panelLayout.min, panelLayout.max),\n        );\n      };\n      const cleanup = () => {\n        window.removeEventListener(\"pointermove\", move);\n        window.removeEventListener(\"pointerup\", cleanup);\n        window.removeEventListener(\"pointercancel\", cleanup);\n        document.body.classList.remove(\"resizing\");\n        dragCleanupRef.current = null;\n      };\n      dragCleanupRef.current?.();\n      dragCleanupRef.current = cleanup;\n      document.body.classList.add(\"resizing\");\n      window.addEventListener(\"pointermove\", move);\n      window.addEventListener(\"pointerup\", cleanup);\n      window.addEventListener(\"pointercancel\", cleanup);\n    },\n    children: (\n      <>\n        {children}\n        <span\n          aria-hidden\n          className=\"absolute inset-y-0 left-1/2 w-0.5 -translate-x-1/2 bg-hairline-strong opacity-0 transition-opacity duration-[var(--duration-state)] ease-[var(--ease-standard)] group-hover:opacity-100 group-focus-visible:opacity-100\"\n        />\n      </>\n    ),\n  };\n\n  const handle = useRender({\n    defaultTagName: \"div\",\n    render,\n    props: mergeProps<\"div\">(props, defaultProps),\n  });\n\n  return panelLayout.open ? handle : null;\n}\n\nexport interface ResizablePanelsProps\n  extends useRender.ComponentProps<\"div\"> {\n  centerFloor: number;\n}\n\nexport function ResizablePanels({\n  centerFloor,\n  className,\n  children,\n  render,\n  style,\n  ...props\n}: ResizablePanelsProps): React.ReactElement {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [containerWidth, setContainerWidth] = useState(0);\n  const childArray = Children.toArray(children);\n  const sidePanels = childArray.filter(\n    (child): child is React.ReactElement<SidePanelProps> =>\n      isValidElement(child) && child.type === SidePanel,\n  );\n  const leftProps = sidePanels.find((panel) => panel.props.side === \"left\")?.props;\n  const rightProps = sidePanels.find(\n    (panel) => panel.props.side === \"right\",\n  )?.props;\n\n  useLayoutEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    const updateWidth = () => setContainerWidth(root.clientWidth);\n    updateWidth();\n    const observer = new ResizeObserver(updateWidth);\n    observer.observe(root);\n    return () => observer.disconnect();\n  }, []);\n\n  const layout = (() => {\n    const available = Math.max(0, containerWidth - centerFloor);\n    let leftWidth = leftProps\n      ? clamp(leftProps.width, leftProps.min, leftProps.max)\n      : 0;\n    let rightWidth = rightProps\n      ? clamp(rightProps.width, rightProps.min, rightProps.max)\n      : 0;\n    const openLeftWidth = leftProps?.open ? leftWidth : 0;\n    const openRightWidth = rightProps?.open ? rightWidth : 0;\n    const overflow = Math.max(\n      0,\n      openLeftWidth + openRightWidth - available,\n    );\n    const leftSlack = leftProps?.open\n      ? Math.max(0, leftWidth - leftProps.min)\n      : 0;\n    const rightSlack = rightProps?.open\n      ? Math.max(0, rightWidth - rightProps.min)\n      : 0;\n    const totalSlack = leftSlack + rightSlack;\n\n    if (overflow > 0 && totalSlack > 0) {\n      const reducibleOverflow = Math.min(overflow, totalSlack);\n      leftWidth -= reducibleOverflow * (leftSlack / totalSlack);\n      rightWidth -= reducibleOverflow * (rightSlack / totalSlack);\n    }\n\n    const leftMax = leftProps\n      ? Math.max(\n          leftProps.min,\n          Math.min(\n            leftProps.max,\n            available - (rightProps?.open ? rightWidth : 0),\n          ),\n        )\n      : 0;\n    const rightMax = rightProps\n      ? Math.max(\n          rightProps.min,\n          Math.min(\n            rightProps.max,\n            available - (leftProps?.open ? leftWidth : 0),\n          ),\n        )\n      : 0;\n\n    return {\n      left: leftProps\n        ? {\n            width: leftWidth,\n            min: leftProps.min,\n            max: leftMax,\n            open: leftProps.open,\n            onResize: leftProps.onResize,\n          }\n        : undefined,\n      right: rightProps\n        ? {\n            width: rightWidth,\n            min: rightProps.min,\n            max: rightMax,\n            open: rightProps.open,\n            onResize: rightProps.onResize,\n          }\n        : undefined,\n    };\n  })();\n\n  const centerChildren = childArray.filter(\n    (child) =>\n      !(\n        isValidElement(child) &&\n        (child.type === SidePanel || child.type === ResizeHandle)\n      ),\n  );\n  const handles = childArray.filter(\n    (child) => isValidElement(child) && child.type === ResizeHandle,\n  );\n  const leftTrack = leftProps?.open ? layout.left?.width ?? 0 : 0;\n  const rightTrack = rightProps?.open ? layout.right?.width ?? 0 : 0;\n\n  const defaultProps = {\n    ref: rootRef,\n    \"data-slot\": \"resizable-panels\",\n    className: cn(\"relative grid min-h-0 min-w-0\", className),\n    style: {\n      ...style,\n      gridTemplateColumns: `${leftTrack}px minmax(${centerFloor}px, 1fr) ${rightTrack}px`,\n      transition:\n        \"grid-template-columns var(--duration-panel) var(--ease-panel)\",\n    },\n    children: (\n      <>\n        {sidePanels}\n        {handles}\n        <div\n          data-slot=\"resizable-panels-center\"\n          className=\"col-start-2 row-start-1 min-h-0 min-w-0\"\n        >\n          {centerChildren}\n        </div>\n      </>\n    ),\n  };\n  const root = useRender({\n    defaultTagName: \"div\",\n    render,\n    props: mergeProps<\"div\">(props, defaultProps),\n  });\n\n  return (\n    <ResizablePanelsContext.Provider value={layout}>\n      {root}\n    </ResizablePanelsContext.Provider>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/resizable-panels.tsx"
    }
  ],
  "type": "registry:ui"
}