{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "side-panel",
  "title": "Side Panel",
  "description": "A pane that comes in from the side: an inspector, a filter set, a row's detail.",
  "dependencies": [
    "@base-ui/react@^1.7.0",
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/side-panel/side-panel.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Dialog } from \"@base-ui/react/dialog\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type SidePanelSide = \"left\" | \"right\"\n\nexport type SidePanelProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  /**\n   * true traps focus and locks the page. \"trap-focus\" keeps the page usable\n   * behind it, for an inspector you work beside rather than instead of.\n   */\n  modal?: boolean | \"trap-focus\"\n  /** Close when the backdrop is pressed. Defaults to true. */\n  dismissible?: boolean\n  /** Close on Escape. Turn it off where there is unsaved work. */\n  closeOnEscape?: boolean\n  hideBackdrop?: boolean\n  /** How far a panel opened inside another sits from the edge. */\n  stackOffset?: string\n  /** Milliseconds the slide takes. Defaults to 250. */\n  duration?: number\n  /** Which edge it comes from. Defaults to the right. */\n  side?: SidePanelSide\n  title: React.ReactNode\n  /** A line under the title, and the dialog's description. */\n  description?: React.ReactNode\n  /** Pinned above the content: filters, a search, a tab strip. */\n  toolbar?: React.ReactNode\n  /** Pinned below it: the actions that close or apply. */\n  footer?: React.ReactNode\n  children?: React.ReactNode\n  closeLabel?: string\n  className?: string\n  /** Width at the medium breakpoint and up. Full width below it. */\n  width?: string\n}\n\n/**\n * A pane that comes in from the side: an inspector, a filter set, the detail\n * of the row you clicked. Base UI supplies the dialog, so the focus trap, the\n * scroll lock, Escape, and focus restoration are not ours to get wrong.\n */\n/** Panels opened inside a panel stack, each one further from the edge. */\nconst SidePanelDepth = React.createContext(0)\n\nexport function SidePanel({\n  open,\n  onOpenChange,\n  modal = true,\n  dismissible = true,\n  closeOnEscape = true,\n  hideBackdrop = false,\n  stackOffset = \"1.75rem\",\n  duration = 250,\n  side = \"right\",\n  title,\n  description,\n  toolbar,\n  footer,\n  children,\n  closeLabel = \"Close\",\n  className,\n  width = \"28rem\",\n}: SidePanelProps) {\n  const depth = React.useContext(SidePanelDepth)\n\n  return (\n    <Dialog.Root\n      open={open}\n      modal={modal}\n      disablePointerDismissal={!dismissible}\n      onOpenChange={(next, details) => {\n        // Escape is the browser's, so refusing it has to be done here.\n        if (!next && !closeOnEscape && details.reason === \"escape-key\") {\n          details.cancel()\n          return\n        }\n\n        onOpenChange(next)\n      }}\n    >\n      <Dialog.Portal>\n        {!hideBackdrop && (\n          <Dialog.Backdrop\n            data-slot=\"side-panel-backdrop\"\n            className=\"bg-foreground/40 fixed inset-0 z-[100] transition-opacity duration-200 data-[closed]:pointer-events-none data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none\"\n          />\n        )}\n        <Dialog.Viewport\n          data-slot=\"side-panel-viewport\"\n          className={cn(\n            \"fixed inset-y-0 z-[101] flex max-w-full data-[closed]:pointer-events-none\",\n            side === \"right\" ? \"right-0\" : \"left-0\"\n          )}\n        >\n          <Dialog.Popup\n            data-slot=\"side-panel\"\n            data-side={side}\n            data-depth={depth || undefined}\n            style={\n              {\n                \"--side-panel-width\": width,\n                \"--side-panel-duration\": `${duration}ms`,\n                // Each panel opened inside another sits this much further from\n                // the edge, so the ones behind stay visible as strips.\n                [side === \"right\" ? \"marginRight\" : \"marginLeft\"]:\n                  depth > 0 ? `calc(${stackOffset} * ${depth})` : undefined,\n              } as React.CSSProperties\n            }\n            className={cn(\n              \"bg-card text-card-foreground border-border flex h-full w-screen flex-col shadow-2xl transition-[opacity,translate] duration-[var(--side-panel-duration)] md:w-[var(--side-panel-width)]\",\n              side === \"right\"\n                ? \"border-l data-[ending-style]:translate-x-full data-[starting-style]:translate-x-full\"\n                : \"border-r data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full\",\n              \"data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none\",\n              className\n            )}\n          >\n            <header\n              data-slot=\"side-panel-header\"\n              className=\"border-border flex items-start gap-3 border-b px-4 py-3\"\n            >\n              <div className=\"min-w-0 flex-1\">\n                <Dialog.Title className=\"truncate text-sm font-semibold\">\n                  {title}\n                </Dialog.Title>\n                {description && (\n                  <Dialog.Description className=\"text-muted-foreground mt-0.5 text-xs leading-relaxed\">\n                    {description}\n                  </Dialog.Description>\n                )}\n              </div>\n              <Dialog.Close\n                aria-label={closeLabel}\n                className=\"text-muted-foreground hover:bg-muted hover:text-foreground -mr-1 inline-flex size-8 shrink-0 items-center justify-center rounded-md transition-colors duration-150 motion-reduce:transition-none\"\n              >\n                <X aria-hidden=\"true\" size={16} />\n              </Dialog.Close>\n            </header>\n\n            {toolbar && (\n              <div\n                data-slot=\"side-panel-toolbar\"\n                className=\"border-border border-b px-4 py-2.5\"\n              >\n                {toolbar}\n              </div>\n            )}\n\n            {/* The body scrolls; the header, toolbar, and footer stay put. */}\n            <div\n              data-slot=\"side-panel-body\"\n              className=\"min-h-0 flex-1 overflow-y-auto px-4 py-4\"\n            >\n              <SidePanelDepth.Provider value={depth + 1}>\n                {children}\n              </SidePanelDepth.Provider>\n            </div>\n\n            {footer && (\n              <div\n                data-slot=\"side-panel-footer\"\n                className=\"border-border flex items-center justify-end gap-2 border-t px-4 py-3\"\n              >\n                {footer}\n              </div>\n            )}\n          </Dialog.Popup>\n        </Dialog.Viewport>\n      </Dialog.Portal>\n    </Dialog.Root>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "overlay",
    "layout",
    "dialog"
  ],
  "type": "registry:ui"
}