{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dockable-panel",
  "type": "registry:ui",
  "title": "Dockable Panel",
  "description": "Non-modal workspace panel that moves between two explicit edge slots with a mobile Drawer fallback.",
  "dependencies": [],
  "registryDependencies": [
    "http://127.0.0.1:3000/r/button.json",
    "http://127.0.0.1:3000/r/core.json",
    "http://127.0.0.1:3000/r/drawer.json",
    "http://127.0.0.1:3000/r/sidebar.json"
  ],
  "files": [
    {
      "path": "src/registry/sources/control-ui/ui/dockable-panel-geometry.ts",
      "target": "@components/control-ui/ui/dockable-panel-geometry.ts",
      "type": "registry:ui",
      "content": "export type DockablePanelBounds = { width: number; height: number };\nexport type DockablePanelSize = { width: number; height: number };\nexport type DockablePanelPosition = { x: number; y: number };\n\nexport function clampDockablePanelPosition(\n  position: DockablePanelPosition,\n  bounds: DockablePanelBounds,\n  panel: DockablePanelSize,\n  inset = 12,\n): DockablePanelPosition {\n  const maxX = Math.max(inset, bounds.width - panel.width - inset);\n  const maxY = Math.max(inset, bounds.height - panel.height - inset);\n\n  return {\n    x: Math.min(Math.max(position.x, inset), maxX),\n    y: Math.min(Math.max(position.y, inset), maxY),\n  };\n}\n\nexport function dockablePanelSideAt(pointerX: number, width: number): \"left\" | \"right\" {\n  return pointerX < width / 2 ? \"left\" : \"right\";\n}\n\nexport function oppositeDockablePanelSide(placement: \"left\" | \"right\"): \"left\" | \"right\" {\n  return placement === \"left\" ? \"right\" : \"left\";\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/ui/dockable-panel.tsx",
      "target": "@components/control-ui/ui/dockable-panel.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport type { PointerEvent as ReactPointerEvent } from \"react\";\nimport { createContext, use, useRef, useState } from \"react\";\nimport type {\n  DockablePanelActionsProps,\n  DockablePanelCloseProps,\n  DockablePanelContentProps,\n  DockablePanelDockProps,\n  DockablePanelDragHandleProps,\n  DockablePanelHeaderProps,\n  DockablePanelPlacement,\n  DockablePanelProps,\n  DockablePanelTitleProps,\n  DockablePanelToggleProps,\n} from \"@/components/control-ui/contracts\";\nimport { useIsMobile } from \"@/components/control-ui/hooks/use-mobile\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport { skinSlot } from \"@/components/control-ui/skin\";\nimport { Button } from \"@/components/control-ui/ui/button\";\nimport { Drawer, DrawerContent } from \"@/components/control-ui/ui/drawer\";\nimport { clampDockablePanelPosition, dockablePanelSideAt, oppositeDockablePanelSide } from \"./dockable-panel-geometry\";\n\ntype DockablePanelContextValue = {\n  close: () => void;\n  dragging: boolean;\n  isMobile: boolean;\n  placement: DockablePanelPlacement;\n  setPlacement: (placement: DockablePanelPlacement) => void;\n  togglePlacement: () => void;\n  dragHandleProps: {\n    onPointerDown: (event: ReactPointerEvent<HTMLButtonElement>) => void;\n    onPointerMove: (event: ReactPointerEvent<HTMLButtonElement>) => void;\n    onPointerUp: (event: ReactPointerEvent<HTMLButtonElement>) => void;\n    onPointerCancel: (event: ReactPointerEvent<HTMLButtonElement>) => void;\n  };\n};\n\nconst DockablePanelContext = createContext<DockablePanelContextValue | null>(null);\n\nfunction useDockablePanelContext() {\n  const context = use(DockablePanelContext);\n  if (!context) throw new Error(\"DockablePanel parts must be used inside DockablePanel.\");\n  return context;\n}\n\nfunction useControllableState<T>(controlled: T | undefined, defaultValue: T, onChange?: (value: T) => void) {\n  const [uncontrolled, setUncontrolled] = useState(defaultValue);\n  const value = controlled ?? uncontrolled;\n  function setValue(next: T) {\n    if (controlled === undefined) setUncontrolled(next);\n    onChange?.(next);\n  }\n  return [value, setValue] as const;\n}\n\nexport function DockablePanel({\n  open: openProp,\n  defaultOpen = true,\n  onOpenChange,\n  placement: placementProp,\n  defaultPlacement = \"right\",\n  onPlacementChange,\n  className,\n  style,\n  children,\n  \"aria-label\": ariaLabel = \"Dockable panel\",\n  ...props\n}: DockablePanelProps) {\n  const [open, setOpen] = useControllableState(openProp, defaultOpen, onOpenChange);\n  const [placement, setPlacementState] = useControllableState(placementProp, defaultPlacement, onPlacementChange);\n  const [dragging, setDragging] = useState(false);\n  const [potentialDock, setPotentialDock] = useState<DockablePanelPlacement>(placement);\n  const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null);\n  const [dragHeight, setDragHeight] = useState(0);\n  const isMobile = useIsMobile();\n  const panelRef = useRef<HTMLElement>(null);\n  const potentialDockRef = useRef<DockablePanelPlacement>(placement);\n  const dragRef = useRef<{ pointerId: number; offsetX: number; offsetY: number } | null>(null);\n\n  function setPlacement(next: DockablePanelPlacement) {\n    setPlacementState(next);\n  }\n\n  function togglePlacement() {\n    setPlacement(oppositeDockablePanelSide(placement));\n  }\n\n  function close() {\n    setOpen(false);\n  }\n\n  function finishDrag(event: ReactPointerEvent<HTMLButtonElement>, shouldDock: boolean) {\n    if (dragRef.current?.pointerId !== event.pointerId) return;\n    if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);\n    if (shouldDock) setPlacement(potentialDockRef.current);\n    dragRef.current = null;\n    setDragPosition(null);\n    setDragHeight(0);\n    setDragging(false);\n  }\n\n  function onPointerDown(event: ReactPointerEvent<HTMLButtonElement>) {\n    if (event.button !== 0 || isMobile) return;\n    const panel = panelRef.current;\n    const container = panel?.offsetParent;\n    if (!(container instanceof HTMLElement) || !panel) return;\n\n    event.preventDefault();\n    event.currentTarget.setPointerCapture(event.pointerId);\n    const panelRect = panel.getBoundingClientRect();\n    const containerRect = container.getBoundingClientRect();\n    dragRef.current = {\n      pointerId: event.pointerId,\n      offsetX: event.clientX - panelRect.left,\n      offsetY: event.clientY - panelRect.top,\n    };\n    potentialDockRef.current = placement;\n    setPotentialDock(placement);\n    setDragPosition({ x: panelRect.left - containerRect.left, y: panelRect.top - containerRect.top });\n    setDragHeight(panelRect.height);\n    setDragging(true);\n  }\n\n  function onPointerMove(event: ReactPointerEvent<HTMLButtonElement>) {\n    const drag = dragRef.current;\n    const panel = panelRef.current;\n    const container = panel?.offsetParent;\n    if (!drag || drag.pointerId !== event.pointerId || !(container instanceof HTMLElement) || !panel) return;\n\n    const bounds = container.getBoundingClientRect();\n    const nextDock = dockablePanelSideAt(event.clientX - bounds.left, bounds.width);\n    potentialDockRef.current = nextDock;\n    setPotentialDock(nextDock);\n    setDragPosition(\n      clampDockablePanelPosition(\n        { x: event.clientX - bounds.left - drag.offsetX, y: event.clientY - bounds.top - drag.offsetY },\n        { width: bounds.width, height: bounds.height },\n        { width: panel.offsetWidth, height: panel.offsetHeight },\n      ),\n    );\n  }\n\n  function onPointerUp(event: ReactPointerEvent<HTMLButtonElement>) {\n    finishDrag(event, true);\n  }\n\n  function onPointerCancel(event: ReactPointerEvent<HTMLButtonElement>) {\n    finishDrag(event, false);\n  }\n\n  const context: DockablePanelContextValue = {\n    close,\n    dragging,\n    isMobile,\n    placement,\n    setPlacement,\n    togglePlacement,\n    dragHandleProps: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel },\n  };\n\n  if (isMobile) {\n    return (\n      <DockablePanelContext.Provider value={context}>\n        <Drawer open={open} onOpenChange={setOpen} side=\"bottom\">\n          <DrawerContent\n            data-control-ui=\"dockable-panel\"\n            data-slot=\"root\"\n            data-surface=\"panel\"\n            data-placement={placement}\n            padding=\"none\"\n            variant=\"floating\"\n            surface=\"card\"\n            aria-label={ariaLabel}\n            className={cn(\"max-h-[min(82vh,42rem)] gap-0 overflow-hidden\", className)}\n            style={style}\n            {...props}\n          >\n            {children}\n          </DrawerContent>\n        </Drawer>\n      </DockablePanelContext.Provider>\n    );\n  }\n\n  if (!open) return null;\n\n  return (\n    <DockablePanelContext.Provider value={context}>\n      {dragging ? (\n        <>\n          <DockablePanelDropZone side=\"left\" active={potentialDock === \"left\"} height={dragHeight} />\n          <DockablePanelDropZone side=\"right\" active={potentialDock === \"right\"} height={dragHeight} />\n        </>\n      ) : null}\n      <aside\n        ref={panelRef}\n        data-control-ui=\"dockable-panel\"\n        data-slot=\"root\"\n        data-surface=\"panel\"\n        data-placement={placement}\n        data-dragging={dragging ? \"true\" : undefined}\n        aria-label={ariaLabel}\n        className={cn(\n          \"absolute z-30 flex w-[min(22rem,calc(50%_-_1.125rem))] flex-col overflow-hidden rounded-[var(--radius-panel)] bg-card text-card-foreground shadow-pop ring-1 ring-border/80 outline-none\",\n          !dragging && placement === \"left\" && \"top-3 left-3 max-h-[calc(100%_-_1.5rem)]\",\n          !dragging && placement === \"right\" && \"top-3 right-3 max-h-[calc(100%_-_1.5rem)]\",\n          dragging && \"top-0 left-0 max-h-[calc(100%_-_1.5rem)]\",\n          dragging && \"select-none shadow-modal\",\n          skinSlot(\"dockable-panel\", \"root\", { placement, dragging }),\n          className,\n        )}\n        style={dragging && dragPosition ? { ...style, transform: `translate3d(${dragPosition.x}px, ${dragPosition.y}px, 0)` } : style}\n        {...props}\n      >\n        {children}\n        <span className=\"sr-only\" aria-live=\"polite\">\n          Panel docked {placement}.\n        </span>\n      </aside>\n    </DockablePanelContext.Provider>\n  );\n}\n\nexport function DockablePanelHeader({ className, ...props }: DockablePanelHeaderProps) {\n  return (\n    <div\n      data-control-ui=\"dockable-panel\"\n      data-slot=\"header\"\n      className={cn(\n        \"flex min-h-11 shrink-0 items-center gap-2 border-b border-border/70 px-2\",\n        skinSlot(\"dockable-panel\", \"header\", {}),\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function DockablePanelDragHandle({\n  className,\n  onPointerDown,\n  onPointerMove,\n  onPointerUp,\n  onPointerCancel,\n  children,\n  ...props\n}: DockablePanelDragHandleProps) {\n  const context = useDockablePanelContext();\n  if (context.isMobile) {\n    return (\n      <div\n        data-control-ui=\"dockable-panel\"\n        data-slot=\"drag-handle\"\n        className={cn(\n          \"flex min-w-0 flex-1 items-center self-stretch px-2 text-left\",\n          skinSlot(\"dockable-panel\", \"drag-handle\", { dragging: false }),\n          className,\n        )}\n      >\n        {children}\n      </div>\n    );\n  }\n\n  return (\n    <button\n      type=\"button\"\n      data-control-ui=\"dockable-panel\"\n      data-slot=\"drag-handle\"\n      aria-label=\"Drag panel\"\n      aria-pressed={context.dragging}\n      className={cn(\n        \"flex min-w-0 flex-1 cursor-grab touch-none items-center self-stretch rounded-[var(--radius-control)] px-2 text-left active:cursor-grabbing focus-visible:ring-2 focus-visible:ring-foreground/20 focus-visible:outline-none\",\n        skinSlot(\"dockable-panel\", \"drag-handle\", { dragging: context.dragging }),\n        className,\n      )}\n      onPointerDown={(event) => {\n        onPointerDown?.(event);\n        if (!event.defaultPrevented) context.dragHandleProps.onPointerDown(event);\n      }}\n      onPointerMove={(event) => {\n        onPointerMove?.(event);\n        if (!event.defaultPrevented) context.dragHandleProps.onPointerMove(event);\n      }}\n      onPointerUp={(event) => {\n        onPointerUp?.(event);\n        if (!event.defaultPrevented) context.dragHandleProps.onPointerUp(event);\n      }}\n      onPointerCancel={(event) => {\n        onPointerCancel?.(event);\n        if (!event.defaultPrevented) context.dragHandleProps.onPointerCancel(event);\n      }}\n      {...props}\n    >\n      {children}\n    </button>\n  );\n}\n\nexport function DockablePanelTitle({ className, ...props }: DockablePanelTitleProps) {\n  return (\n    <h2\n      data-control-ui=\"dockable-panel\"\n      data-slot=\"title\"\n      className={cn(\"truncate text-sm font-semibold\", skinSlot(\"dockable-panel\", \"title\", {}), className)}\n      {...props}\n    />\n  );\n}\n\nexport function DockablePanelActions({ className, ...props }: DockablePanelActionsProps) {\n  return (\n    <div\n      data-control-ui=\"dockable-panel\"\n      data-slot=\"actions\"\n      className={cn(\"ml-auto flex shrink-0 items-center gap-0.5\", skinSlot(\"dockable-panel\", \"actions\", {}), className)}\n      {...props}\n    />\n  );\n}\n\nexport function DockablePanelToggle({ className, children, \"aria-label\": ariaLabel, onClick, ...props }: DockablePanelToggleProps) {\n  const { isMobile, placement, togglePlacement } = useDockablePanelContext();\n  if (isMobile) return null;\n  const target = oppositeDockablePanelSide(placement);\n  const label = `Move panel to ${target}`;\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"sm\"\n      iconOnly\n      aria-label={ariaLabel ?? label}\n      title={label}\n      data-control-ui=\"dockable-panel\"\n      data-slot=\"toggle\"\n      className={cn(skinSlot(\"dockable-panel\", \"toggle\", { placement }), className)}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) togglePlacement();\n      }}\n      {...props}\n    >\n      {children ?? <DockPanelIcon side={target} />}\n    </Button>\n  );\n}\n\nexport function DockablePanelDock({\n  placement: nextPlacement,\n  className,\n  children,\n  \"aria-label\": ariaLabel,\n  onClick,\n  ...props\n}: DockablePanelDockProps) {\n  const { isMobile, placement, setPlacement } = useDockablePanelContext();\n  if (isMobile) return null;\n  const label = `Dock panel ${nextPlacement}`;\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"sm\"\n      iconOnly\n      active={placement === nextPlacement}\n      aria-pressed={placement === nextPlacement}\n      aria-label={ariaLabel ?? label}\n      title={label}\n      data-control-ui=\"dockable-panel\"\n      data-slot=\"dock\"\n      className={cn(skinSlot(\"dockable-panel\", \"dock\", { active: placement === nextPlacement, placement: nextPlacement }), className)}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) setPlacement(nextPlacement);\n      }}\n      {...props}\n    >\n      {children ?? <PlacementIcon placement={nextPlacement} />}\n    </Button>\n  );\n}\n\nexport function DockablePanelClose({\n  className,\n  children,\n  \"aria-label\": ariaLabel = \"Close panel\",\n  onClick,\n  ...props\n}: DockablePanelCloseProps) {\n  const { close } = useDockablePanelContext();\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"sm\"\n      iconOnly\n      aria-label={ariaLabel}\n      title={ariaLabel}\n      data-control-ui=\"dockable-panel\"\n      data-slot=\"close\"\n      className={cn(skinSlot(\"dockable-panel\", \"close\", {}), className)}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) close();\n      }}\n      {...props}\n    >\n      {children ?? <CloseIcon />}\n    </Button>\n  );\n}\n\nexport function DockablePanelContent({ padding = \"default\", className, ...props }: DockablePanelContentProps) {\n  return (\n    <div\n      data-control-ui=\"dockable-panel\"\n      data-slot=\"content\"\n      data-padding={padding}\n      className={cn(\n        \"min-h-0 flex-1 overflow-y-auto\",\n        padding === \"default\" && \"p-3\",\n        skinSlot(\"dockable-panel\", \"content\", { padding }),\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DockablePanelDropZone({ side, active, height }: { side: \"left\" | \"right\"; active: boolean; height: number }) {\n  return (\n    <div\n      data-control-ui=\"dockable-panel\"\n      data-slot=\"drop-zone\"\n      data-surface=\"panel\"\n      data-side={side}\n      data-active={active ? \"true\" : undefined}\n      className={cn(\n        \"pointer-events-none absolute top-3 z-20 w-[min(22rem,calc(50%_-_1.125rem))] rounded-[var(--radius-panel)] ring-1 transition-[background-color,box-shadow] duration-[var(--duration-fast)]\",\n        active ? \"bg-primary/12 ring-primary/55 shadow-sm\" : \"bg-background/12 ring-foreground/20\",\n        side === \"left\" ? \"left-3\" : \"right-3\",\n        skinSlot(\"dockable-panel\", \"drop-zone\", { side, active }),\n      )}\n      style={{ height }}\n    />\n  );\n}\n\nfunction PlacementIcon({ placement }: { placement: DockablePanelPlacement }) {\n  return <DockPanelIcon side={placement} />;\n}\n\nfunction DockPanelIcon({ side = \"right\" }: { side?: \"left\" | \"right\" }) {\n  return (\n    <svg viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" aria-hidden=\"true\">\n      <rect x=\"2.25\" y=\"2.25\" width=\"11.5\" height=\"11.5\" rx=\"2\" stroke=\"currentColor\" strokeWidth=\"1.25\" />\n      <path d={side === \"left\" ? \"M6 2.75v10.5\" : \"M10 2.75v10.5\"} stroke=\"currentColor\" strokeWidth=\"1.25\" />\n    </svg>\n  );\n}\n\nfunction CloseIcon() {\n  return (\n    <svg viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" aria-hidden=\"true\">\n      <path d=\"m4.5 4.5 7 7m0-7-7 7\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n"
    }
  ],
  "meta": {}
}
