{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "resizable",
  "type": "registry:ui",
  "title": "Resizable Panels",
  "description": "Accessible nested pane groups and recoverable workspaces with focus, collapse and saved layouts.",
  "dependencies": [
    "react-resizable-panels@^4.13.2"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/resizable/resizable.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/resizable/resizable.tsx",
      "content": "\"use client\";\n\nimport {\n  createContext,\n  forwardRef,\n  useContext,\n  useRef,\n  useState,\n  version as reactVersion,\n  type HTMLAttributes,\n  type Ref,\n  type MutableRefObject,\n} from \"react\";\nimport {\n  Group,\n  Panel,\n  Separator,\n  type GroupProps,\n  type PanelProps,\n  type SeparatorProps,\n  type PanelImperativeHandle,\n} from \"react-resizable-panels\";\nimport { cn } from \"../../utils/cn\";\nconst inertAttribute = (\n  Number.parseInt(reactVersion, 10) < 19 ? \"\" : true\n) as true;\n\nconst OrientationContext = createContext<\"horizontal\" | \"vertical\">(\n  \"horizontal\",\n);\nexport type ResizablePanelGroupProps = GroupProps;\nexport type ResizablePanelProps = PanelProps;\nexport type ResizableHandleProps = SeparatorProps & { withHandle?: boolean };\nexport type {\n  GroupImperativeHandle as ResizableGroupHandle,\n  PanelImperativeHandle as ResizablePanelHandle,\n  Layout as ResizableLayout,\n} from \"react-resizable-panels\";\n\nfunction assignRef<T>(ref: Ref<T> | undefined, value: T) {\n  if (typeof ref === \"function\") ref(value);\n  else if (ref) (ref as MutableRefObject<T>).current = value;\n}\n\nexport const ResizablePanelGroup = forwardRef<\n  HTMLDivElement,\n  ResizablePanelGroupProps\n>(function ResizablePanelGroup(\n  {\n    className,\n    orientation = \"horizontal\",\n    resizeTargetMinimumSize = { coarse: 44, fine: 12 },\n    elementRef,\n    onPointerCancel,\n    ...props\n  },\n  ref,\n) {\n  return (\n    <OrientationContext.Provider value={orientation}>\n      <Group\n        {...props}\n        orientation={orientation}\n        resizeTargetMinimumSize={resizeTargetMinimumSize}\n        onPointerCancel={(event) => {\n          onPointerCancel?.(event);\n          // Engine 4.13 listens for pointerup but not pointercancel. Complete\n          // only an active resize in this group, retaining its latest size.\n          // Dispatch on the document so pane content does not receive a fake up.\n          if (event.currentTarget.querySelector('[data-separator=\"active\"]')) {\n            event.currentTarget.ownerDocument.dispatchEvent(\n              new PointerEvent(\"pointerup\", {\n                pointerId: event.pointerId,\n                pointerType: event.pointerType,\n                clientX: event.clientX,\n                clientY: event.clientY,\n                button: 0,\n              }),\n            );\n          }\n        }}\n        elementRef={(node) => {\n          assignRef(ref, node);\n          assignRef(elementRef, node);\n        }}\n        data-slot=\"resizable-group\"\n        data-orientation={orientation}\n        className={cn(\"min-h-0 min-w-0\", className)}\n      />\n    </OrientationContext.Provider>\n  );\n});\n\nexport const ResizablePanel = forwardRef<HTMLDivElement, ResizablePanelProps>(\n  function ResizablePanel(\n    { className, children, panelRef, elementRef, onResize, ...props },\n    ref,\n  ) {\n    const handle = useRef<PanelImperativeHandle | null>(null);\n    const [collapsed, setCollapsed] = useState(false);\n    return (\n      <Panel\n        {...props}\n        elementRef={(node) => {\n          assignRef(ref, node);\n          assignRef(elementRef, node);\n        }}\n        panelRef={(value) => {\n          handle.current = value;\n          assignRef(panelRef, value);\n        }}\n        onResize={(size, id, previous) => {\n          setCollapsed(handle.current?.isCollapsed() ?? false);\n          onResize?.(size, id, previous);\n        }}\n        data-slot=\"resizable-panel\"\n        data-collapsed={collapsed || undefined}\n        className={cn(\n          \"bg-background text-foreground flex min-h-0 min-w-0 flex-col\",\n          className,\n        )}\n      >\n        <div\n          {...{\n            inert: collapsed && props.collapsible ? inertAttribute : undefined,\n          }}\n          aria-hidden={(collapsed && props.collapsible) || undefined}\n          className=\"flex min-h-0 flex-1 flex-col\"\n        >\n          {children}\n        </div>\n      </Panel>\n    );\n  },\n);\n\nexport const ResizableHandle = forwardRef<HTMLDivElement, ResizableHandleProps>(\n  function ResizableHandle(\n    { className, withHandle = true, children, elementRef, ...props },\n    ref,\n  ) {\n    const vertical = useContext(OrientationContext) === \"vertical\";\n    return (\n      <Separator\n        {...props}\n        elementRef={(node) => {\n          assignRef(ref, node);\n          assignRef(elementRef, node);\n        }}\n        data-slot=\"resizable-handle\"\n        className={cn(\n          \"bg-border data-[separator=hover]:bg-primary/50 data-[separator=active]:bg-primary data-[separator=focus]:bg-primary focus-visible:ring-ring focus-visible:ring-offset-background relative z-10 flex shrink-0 touch-none items-center justify-center outline-none focus-visible:ring-2 focus-visible:ring-offset-2 forced-colors:bg-[ButtonBorder]\",\n          vertical ? \"h-px w-full\" : \"h-full w-px\",\n          className,\n        )}\n      >\n        {children ??\n          (withHandle && (\n            <span\n              aria-hidden=\"true\"\n              className={cn(\n                \"border-border bg-background text-muted-foreground pointer-events-none flex items-center justify-center rounded-full border shadow-sm\",\n                vertical ? \"h-3 w-7\" : \"h-7 w-3\",\n              )}\n            >\n              <span\n                className={cn(\n                  \"rounded-full bg-current\",\n                  vertical ? \"h-0.5 w-3\" : \"h-3 w-0.5\",\n                )}\n              />\n            </span>\n          ))}\n      </Separator>\n    );\n  },\n);\n\nexport const ResizablePanelHeader = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function ResizablePanelHeader({ className, ...props }, ref) {\n  return (\n    <div\n      {...props}\n      ref={ref}\n      data-slot=\"resizable-panel-header\"\n      className={cn(\n        \"border-border flex min-h-12 shrink-0 items-center justify-between gap-3 border-b px-[calc(var(--dt-density-gap)+var(--dt-space-2))] py-[var(--dt-density-gap)] text-sm font-medium\",\n        className,\n      )}\n    />\n  );\n});\nexport const ResizablePanelBody = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function ResizablePanelBody({ className, ...props }, ref) {\n  return (\n    <div\n      {...props}\n      ref={ref}\n      data-slot=\"resizable-panel-body\"\n      tabIndex={props.tabIndex ?? 0}\n      className={cn(\n        \"focus-visible:ring-ring min-h-0 min-w-0 flex-1 overflow-auto overscroll-contain p-[calc(var(--dt-density-gap)+var(--dt-space-2))] outline-none focus-visible:ring-2 focus-visible:ring-inset\",\n        className,\n      )}\n    />\n  );\n});\nexport const ResizablePanelFooter = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function ResizablePanelFooter({ className, ...props }, ref) {\n  return (\n    <div\n      {...props}\n      ref={ref}\n      data-slot=\"resizable-panel-footer\"\n      className={cn(\n        \"border-border text-muted-foreground flex shrink-0 items-center justify-between gap-3 border-t px-[calc(var(--dt-density-gap)+var(--dt-space-2))] py-2 text-xs\",\n        className,\n      )}\n    />\n  );\n});\n"
    },
    {
      "path": "packages/components/src/components/resizable/resizable-workspace.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/resizable/resizable-workspace.tsx",
      "content": "\"use client\";\n\nimport {\n  Fragment,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  version as reactVersion,\n  type ReactNode,\n} from \"react\";\nimport type {\n  GroupImperativeHandle,\n  PanelImperativeHandle,\n  Layout,\n} from \"react-resizable-panels\";\nimport { cn } from \"../../utils/cn\";\nconst inertAttribute = (\n  Number.parseInt(reactVersion, 10) < 19 ? \"\" : true\n) as true;\nimport {\n  ResizablePanelGroup,\n  ResizablePanel,\n  ResizableHandle,\n  ResizablePanelHeader,\n  ResizablePanelBody,\n  ResizablePanelFooter,\n} from \"./resizable\";\nimport {\n  parseResizableLayout,\n  serializeResizableLayout,\n  type ResizableLayoutStorage,\n} from \"./resizable-layout\";\n\nexport interface ResizableWorkspacePane {\n  id: string;\n  title: string;\n  children: ReactNode;\n  description?: ReactNode;\n  actions?: ReactNode;\n  footer?: ReactNode;\n  defaultSize?: string | number;\n  minSize?: string | number;\n  maxSize?: string | number;\n  collapsible?: boolean;\n}\nexport interface ResizableWorkspaceProps {\n  /** Stable document-unique ID; also scopes pane IDs in saved layouts. */\n  id: string;\n  label: string;\n  panes: readonly ResizableWorkspacePane[];\n  orientation?: \"horizontal\" | \"vertical\";\n  /** Opt into a stacked, non-resizing layout below this container width. */\n  compactAt?: number;\n  /** Persistence is disabled unless a key is supplied. */\n  storageKey?: string;\n  storage?: ResizableLayoutStorage;\n  onLayoutCommit?: (layout: Layout) => void;\n  className?: string;\n}\n\nconst actionClass =\n  \"inline-flex min-h-8 items-center justify-center rounded-md border border-transparent px-2 text-xs font-medium text-muted-foreground hover:border-border hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:opacity-40\";\n\nexport function ResizableWorkspace({\n  id,\n  label,\n  panes,\n  orientation = \"horizontal\",\n  compactAt,\n  storageKey,\n  storage,\n  onLayoutCommit,\n  className,\n}: ResizableWorkspaceProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const group = useRef<GroupImperativeHandle | null>(null);\n  const handles = useRef(new Map<string, PanelImperativeHandle>());\n  const baseline = useRef<Layout | undefined>(undefined);\n  const normalLayout = useRef<Layout | undefined>(undefined);\n  const ready = useRef(false);\n  const [initializedFor, setInitializedFor] = useState<string | null>(null);\n  const [compact, setCompact] = useState(false);\n  const [focused, setFocused] = useState<string | null>(null);\n  const [collapsed, setCollapsed] = useState<Set<string>>(() => new Set());\n  const returnFocus = useRef<HTMLElement | null>(null);\n  const previousFocus = useRef<string | null>(null);\n  const restoreButton = useRef<HTMLButtonElement>(null);\n  const helperId = useId();\n  const ids = panes.map((pane) => `${id}-${pane.id}`);\n  const signature = JSON.stringify(ids);\n  const active = panes.some((pane) => pane.id === focused) ? focused : null;\n  if (focused && !active) setFocused(null);\n  const getStorage = () =>\n    storage ??\n    (typeof window !== \"undefined\" ? window.localStorage : undefined);\n  const persist = (layout: Layout) => {\n    if (storageKey) {\n      try {\n        getStorage()?.setItem(storageKey, serializeResizableLayout(layout));\n      } catch {\n        /* Private/blocked storage does not disable resizing. */\n      }\n    }\n  };\n\n  useEffect(() => {\n    const node = root.current;\n    if (!node || compactAt === undefined) return;\n    const observer = new ResizeObserver(([entry]) =>\n      setCompact(entry.contentRect.width < compactAt),\n    );\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [compactAt]);\n\n  useEffect(() => {\n    ready.current = false;\n    const frame = requestAnimationFrame(() => {\n      const initial = group.current?.getLayout();\n      baseline.current = initial;\n      let saved: Layout | undefined;\n      try {\n        saved = storageKey\n          ? parseResizableLayout(\n              (storage ?? window.localStorage).getItem(storageKey),\n              JSON.parse(signature) as string[],\n            )\n          : undefined;\n      } catch {\n        /* Storage is best effort. */\n      }\n      normalLayout.current = saved ?? initial;\n      if (saved) group.current?.setLayout(saved);\n      ready.current = true;\n      setInitializedFor(signature);\n    });\n    return () => {\n      cancelAnimationFrame(frame);\n      ready.current = false;\n    };\n  }, [signature, storageKey, storage]);\n\n  useEffect(() => {\n    if (!compact && ready.current && normalLayout.current)\n      group.current?.setLayout(normalLayout.current);\n  }, [compact]);\n\n  useEffect(() => {\n    if (previousFocus.current && !active && !returnFocus.current?.isConnected) {\n      restoreButton.current?.focus();\n    }\n    previousFocus.current = active;\n  }, [active]);\n\n  function restore() {\n    setFocused(null);\n    requestAnimationFrame(() => {\n      if (returnFocus.current?.isConnected) returnFocus.current.focus();\n      else restoreButton.current?.focus();\n    });\n  }\n\n  function focusPane(paneId: string, trigger: HTMLElement) {\n    returnFocus.current = trigger;\n    setFocused(paneId);\n    // The workspace action remains available even when the source pane is hidden.\n    requestAnimationFrame(() => restoreButton.current?.focus());\n  }\n\n  function togglePane(paneId: string) {\n    const handle = handles.current.get(paneId);\n    if (!handle) return;\n    if (handle.isCollapsed()) handle.expand();\n    else handle.collapse();\n  }\n\n  if (\n    panes.length < 2 ||\n    new Set(panes.map((pane) => pane.id)).size !== panes.length ||\n    panes.some((pane) => !pane.id || !pane.title)\n  ) {\n    throw new Error(\n      \"ResizableWorkspace requires at least two panes with unique non-empty IDs and titles.\",\n    );\n  }\n\n  return (\n    <div\n      ref={root}\n      data-slot=\"resizable-workspace\"\n      data-ready={initializedFor === signature || undefined}\n      data-compact={compact || undefined}\n      data-focused={active || undefined}\n      className={cn(\n        \"border-border bg-background text-foreground flex min-h-0 min-w-0 flex-col overflow-hidden rounded-xl border\",\n        className,\n      )}\n    >\n      <div\n        role=\"group\"\n        aria-label={`${label} layout controls`}\n        className=\"border-border bg-muted/30 flex shrink-0 flex-wrap items-center justify-between gap-2 border-b px-3 py-2\"\n      >\n        <span className=\"text-xs font-semibold\">{label}</span>\n        <div className=\"flex flex-wrap items-center gap-1\">\n          {panes\n            .filter((pane) => pane.collapsible)\n            .map((pane) => (\n              <button\n                key={pane.id}\n                type=\"button\"\n                className={actionClass}\n                aria-controls={`${id}-${pane.id}`}\n                aria-expanded={!collapsed.has(pane.id) || compact}\n                disabled={!!active || compact || initializedFor !== signature}\n                onClick={() => togglePane(pane.id)}\n              >\n                {collapsed.has(pane.id) ? \"Show\" : \"Hide\"} {pane.title}\n              </button>\n            ))}\n          <button\n            ref={restoreButton}\n            type=\"button\"\n            disabled={initializedFor !== signature}\n            className={actionClass}\n            onClick={() => {\n              if (active) {\n                restore();\n                return;\n              }\n              if (baseline.current) {\n                normalLayout.current = baseline.current;\n                if (!compact) group.current?.setLayout(baseline.current);\n                persist(baseline.current);\n                onLayoutCommit?.(baseline.current);\n              }\n            }}\n          >\n            {active ? \"Restore layout\" : \"Reset layout\"}\n          </button>\n        </div>\n      </div>\n      <p id={helperId} className=\"sr-only\">\n        Use arrow keys on a divider to resize. Home and End reach size limits.\n        Enter toggles a collapsible pane. Use the layout controls to reopen\n        hidden panes.\n      </p>\n      <div className={cn(\"min-h-0 flex-1\", compact && \"flex-none\")}>\n        <ResizablePanelGroup\n          id={`${id}-group`}\n          groupRef={group}\n          orientation={orientation}\n          disabled={!!active || compact}\n          className={cn(\n            \"relative\",\n            compact &&\n              \"!h-auto !flex-col [&>[data-panel]]:!max-h-none [&>[data-panel]]:!w-full [&>[data-panel]]:!flex-none\",\n            compact &&\n              active &&\n              \"!h-[var(--dt-resizable-compact-height,22rem)]\",\n          )}\n          onLayoutChanged={(layout) => {\n            if (!ready.current || active || compact) return;\n            normalLayout.current = layout;\n            persist(layout);\n            onLayoutCommit?.(layout);\n          }}\n        >\n          {panes.map((pane, index) => (\n            <Fragment key={pane.id}>\n              {index > 0 && (\n                <ResizableHandle\n                  aria-label={panes[index - 1].title}\n                  aria-describedby={helperId}\n                  className={cn((active || compact) && \"invisible\")}\n                />\n              )}\n              <ResizablePanel\n                id={`${id}-${pane.id}`}\n                defaultSize={pane.defaultSize ?? `${100 / panes.length}%`}\n                minSize={pane.minSize ?? \"10%\"}\n                maxSize={pane.maxSize}\n                collapsible={pane.collapsible && !compact}\n                panelRef={(handle) => {\n                  if (handle) handles.current.set(pane.id, handle);\n                  else handles.current.delete(pane.id);\n                }}\n                onResize={() => {\n                  const isCollapsed =\n                    handles.current.get(pane.id)?.isCollapsed() ?? false;\n                  setCollapsed((previous) => {\n                    if (previous.has(pane.id) === isCollapsed) return previous;\n                    const next = new Set(previous);\n                    if (isCollapsed) next.add(pane.id);\n                    else next.delete(pane.id);\n                    return next;\n                  });\n                }}\n                className={cn(\n                  \"!overflow-hidden\",\n                  compact &&\n                    \"border-border !h-[var(--dt-resizable-compact-height,22rem)] !max-h-none border-b\",\n                  active === pane.id &&\n                    \"absolute inset-0 z-20 !max-h-none !max-w-none\",\n                  active && active !== pane.id && \"invisible\",\n                )}\n              >\n                <div\n                  {...{\n                    inert:\n                      active && active !== pane.id ? inertAttribute : undefined,\n                  }}\n                  aria-hidden={active && active !== pane.id ? true : undefined}\n                  className=\"flex min-h-0 flex-1 flex-col\"\n                >\n                  <ResizablePanelHeader>\n                    <div className=\"min-w-0\">\n                      <h3 className=\"truncate\">{pane.title}</h3>\n                      {pane.description && (\n                        <p className=\"text-muted-foreground mt-0.5 truncate text-xs font-normal\">\n                          {pane.description}\n                        </p>\n                      )}\n                    </div>\n                    <div className=\"flex shrink-0 items-center gap-1\">\n                      {pane.actions}\n                      <button\n                        type=\"button\"\n                        className={actionClass}\n                        aria-label={\n                          active === pane.id\n                            ? `Restore ${pane.title}`\n                            : `Focus ${pane.title}`\n                        }\n                        onClick={(event) =>\n                          active\n                            ? restore()\n                            : focusPane(pane.id, event.currentTarget)\n                        }\n                      >\n                        {active === pane.id ? \"Restore\" : \"Focus\"}\n                      </button>\n                    </div>\n                  </ResizablePanelHeader>\n                  <ResizablePanelBody>{pane.children}</ResizablePanelBody>\n                  {pane.footer && (\n                    <ResizablePanelFooter>{pane.footer}</ResizablePanelFooter>\n                  )}\n                </div>\n              </ResizablePanel>\n            </Fragment>\n          ))}\n        </ResizablePanelGroup>\n      </div>\n    </div>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/resizable/resizable-layout.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/resizable/resizable-layout.ts",
      "content": "import type { Layout } from \"react-resizable-panels\";\n\n/** Storage is optional; applications can supply localStorage, sessionStorage or memory. */\nexport type ResizableLayoutStorage = Pick<\n  Storage,\n  \"getItem\" | \"setItem\" | \"removeItem\"\n>;\n\nexport function parseResizableLayout(\n  serialized: string | null,\n  panelIds: readonly string[],\n): Layout | undefined {\n  if (!serialized || !panelIds.length) return;\n  try {\n    const value: unknown = JSON.parse(serialized);\n    if (\n      !value ||\n      typeof value !== \"object\" ||\n      !(\"version\" in value) ||\n      value.version !== 1 ||\n      !(\"layout\" in value)\n    )\n      return;\n    const layout = value.layout;\n    if (!layout || typeof layout !== \"object\" || Array.isArray(layout)) return;\n    const entries = Object.entries(layout);\n    if (\n      entries.length !== panelIds.length ||\n      new Set(panelIds).size !== panelIds.length\n    )\n      return;\n    if (\n      !entries.every(\n        ([id, size]) =>\n          panelIds.includes(id) &&\n          typeof size === \"number\" &&\n          Number.isFinite(size) &&\n          size >= 0 &&\n          size <= 100,\n      )\n    )\n      return;\n    if (\n      Math.abs(\n        entries.reduce((sum, [, size]) => sum + (size as number), 0) - 100,\n      ) > 0.1\n    )\n      return;\n    return Object.fromEntries(entries) as Layout;\n  } catch {\n    return;\n  }\n}\n\nexport function serializeResizableLayout(layout: Layout): string {\n  return JSON.stringify({ version: 1, layout });\n}\n"
    },
    {
      "path": "packages/components/src/components/resizable/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/resizable/index.ts",
      "content": "export * from \"./resizable\";\nexport * from \"./resizable-workspace\";\nexport {\n  parseResizableLayout,\n  serializeResizableLayout,\n  type ResizableLayoutStorage,\n} from \"./resizable-layout\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
