{
  "schemaVersion": 1,
  "name": "resizable",
  "useFor": "Use for a workspace whose panes people size themselves (the inbox list beside a message, a log beside its detail). Give each ResizablePanel a defaultSize in percent and sensible minSize; add withHandle when the edge needs to be found.",
  "constraints": [
    "Thick or colored dividers",
    "Panels that can collapse to nothing without a minSize",
    "Animating the panes while dragging"
  ],
  "documentation": "",
  "source": "Extrapolated from the Inkbox console (Figma 427:2715): the 1px rules between the rails and the inbox list",
  "exports": [
    "ResizableHandle",
    "ResizablePanel",
    "ResizablePanelGroup"
  ],
  "client": true,
  "docsUrl": "https://inkbox-ds.vercel.app/components/resizable",
  "markdownUrl": "https://inkbox-ds.vercel.app/r/resizable.md",
  "examples": [
    {
      "path": "examples/resizable.tsx",
      "content": "import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from \"@/components/ui/resizable\";\n\nfunction Pane({ label, className }: { label: string; className?: string }) {\n  return <div className={\"flex h-full items-center justify-center p-6 text-body text-fg-muted \" + (className ?? \"\")}>{label}</div>;\n}\n\nexport function Preview() {\n  return (\n    <ResizablePanelGroup direction=\"horizontal\" className=\"min-h-64 max-w-2xl overflow-hidden rounded-panel border border-line-subtle\">\n      <ResizablePanel defaultSize={36} minSize={20}>\n        <Pane label=\"Inbox\" className=\"bg-subtle\" />\n      </ResizablePanel>\n      <ResizableHandle withHandle />\n      <ResizablePanel defaultSize={64} minSize={30}>\n        <Pane label=\"Message\" />\n      </ResizablePanel>\n    </ResizablePanelGroup>\n  );\n}\n\nexport function Nested() {\n  return (\n    <ResizablePanelGroup direction=\"horizontal\" className=\"min-h-72 max-w-2xl overflow-hidden rounded-panel border border-line-subtle\">\n      <ResizablePanel defaultSize={30} minSize={15}>\n        <Pane label=\"Agents\" className=\"bg-subtle\" />\n      </ResizablePanel>\n      <ResizableHandle />\n      <ResizablePanel defaultSize={70}>\n        <ResizablePanelGroup direction=\"vertical\">\n          <ResizablePanel defaultSize={40} minSize={20}>\n            <Pane label=\"Delivery log\" />\n          </ResizablePanel>\n          <ResizableHandle withHandle />\n          <ResizablePanel defaultSize={60} minSize={20}>\n            <Pane label=\"Payload\" />\n          </ResizablePanel>\n        </ResizablePanelGroup>\n      </ResizablePanel>\n    </ResizablePanelGroup>\n  );\n}\n",
      "exports": [
        "Preview",
        "Nested"
      ],
      "requires": [
        "resizable"
      ],
      "packages": [],
      "previews": [
        {
          "name": "Preview",
          "url": "https://inkbox-ds.vercel.app/preview/resizable/Preview"
        },
        {
          "name": "Nested",
          "url": "https://inkbox-ds.vercel.app/preview/resizable/Nested"
        }
      ]
    }
  ],
  "type": "ui",
  "title": "Resizable",
  "description": "Panels split by the console's 1px lilac rule, which can be dragged or moved with the arrow keys.",
  "surface": "site",
  "kinds": [],
  "requires": [
    "theme",
    "utils"
  ],
  "packages": [
    "@phosphor-icons/react"
  ],
  "files": [
    {
      "path": "components/ui/resizable.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { DotsSixVerticalIcon } from \"@phosphor-icons/react/ssr\";\n\nimport { cn } from \"@/lib/utils\";\n\n// Panels with a draggable edge between them (extrapolated from the Inkbox console, Figma 427:2715: the 1px rules\n// between its rails and the inbox list). The handle is the console's 1px lilac rule with a wide invisible grip; with\n// `withHandle` it carries a 12 by 20 white tab with 4px corners, the field edge and a grip glyph. Drag it, or focus it\n// and use the arrow keys (Home and End go to the limits). Sizes are percentages of the group; dragging follows the\n// pointer directly, with no animation.\n\ntype Direction = \"horizontal\" | \"vertical\";\ntype PanelInfo = { id: string; defaultSize?: number; minSize: number; maxSize: number; order: number };\ntype GroupContext = {\n  direction: Direction;\n  sizes: Record<string, number>;\n  register: (info: PanelInfo) => () => void;\n  resize: (handleEl: HTMLElement, delta: number) => void;\n  set: (handleEl: HTMLElement, to: \"min\" | \"max\") => void;\n  limits: (handleEl: HTMLElement) => { before?: string; value: number; min: number; max: number };\n};\n\nconst ResizableContext = React.createContext<GroupContext | null>(null);\nconst useGroup = () => {\n  const ctx = React.useContext(ResizableContext);\n  if (!ctx) throw new Error(\"Resizable parts must be used within a <ResizablePanelGroup />\");\n  return ctx;\n};\n\nfunction ResizablePanelGroup({ direction = \"horizontal\", className, onLayout, children, ...props }: Omit<React.ComponentProps<\"div\">, \"dir\"> & { direction?: Direction; onLayout?: (sizes: number[]) => void }) {\n  const [panels, setPanels] = React.useState<PanelInfo[]>([]);\n  const [sizes, setSizes] = React.useState<Record<string, number>>({});\n  const group = React.useRef<HTMLDivElement>(null);\n  const counter = React.useRef(0);\n\n  const register = React.useCallback((info: PanelInfo) => {\n    setPanels((p) => [...p.filter((x) => x.id !== info.id), { ...info, order: counter.current++ }]);\n    return () => setPanels((p) => p.filter((x) => x.id !== info.id));\n  }, []);\n\n  // Panels without a default size share what the others leave.\n  React.useEffect(() => {\n    setSizes((current) => {\n      const ids = panels.map((p) => p.id);\n      if (ids.every((id) => id in current) && Object.keys(current).length === ids.length) return current;\n      const fixed = panels.filter((p) => p.defaultSize !== undefined).reduce((s, p) => s + (p.defaultSize ?? 0), 0);\n      const free = panels.filter((p) => p.defaultSize === undefined).length;\n      return Object.fromEntries(panels.map((p) => [p.id, p.defaultSize ?? Math.max(0, (100 - fixed) / Math.max(1, free))]));\n    });\n  }, [panels]);\n\n  const ordered = React.useCallback(() => {\n    const els = Array.from(group.current?.querySelectorAll<HTMLElement>(\":scope > [data-slot=resizable-panel]\") ?? []);\n    return els.map((el) => el.dataset.panelId!);\n  }, []);\n\n  const neighbors = React.useCallback((handleEl: HTMLElement) => {\n    let prev = handleEl.previousElementSibling as HTMLElement | null;\n    while (prev && prev.dataset.slot !== \"resizable-panel\") prev = prev.previousElementSibling as HTMLElement | null;\n    let next = handleEl.nextElementSibling as HTMLElement | null;\n    while (next && next.dataset.slot !== \"resizable-panel\") next = next.nextElementSibling as HTMLElement | null;\n    return [prev?.dataset.panelId, next?.dataset.panelId] as const;\n  }, []);\n\n  const info = (id?: string) => panels.find((p) => p.id === id);\n\n  const apply = React.useCallback((a: string, b: string, nextA: number) => {\n    setSizes((s) => {\n      const pa = panels.find((p) => p.id === a);\n      const pb = panels.find((p) => p.id === b);\n      if (!pa || !pb) return s;\n      const total = s[a] + s[b];\n      const lo = Math.max(pa.minSize, total - pb.maxSize);\n      const hi = Math.min(pa.maxSize, total - pb.minSize);\n      const clamped = Math.min(hi, Math.max(lo, nextA));\n      if (clamped === s[a]) return s;\n      const out = { ...s, [a]: clamped, [b]: total - clamped };\n      onLayout?.(ordered().map((id) => out[id]));\n      return out;\n    });\n  }, [onLayout, ordered, panels]);\n\n  const resize = React.useCallback((handleEl: HTMLElement, delta: number) => {\n    const [a, b] = neighbors(handleEl);\n    if (a && b) apply(a, b, (sizes[a] ?? 0) + delta);\n  }, [apply, neighbors, sizes]);\n\n  const set = React.useCallback((handleEl: HTMLElement, to: \"min\" | \"max\") => {\n    const [a, b] = neighbors(handleEl);\n    if (a && b) apply(a, b, to === \"min\" ? 0 : 100);\n  }, [apply, neighbors]);\n\n  const limits = (handleEl: HTMLElement) => {\n    const [a] = neighbors(handleEl);\n    const p = info(a);\n    return { before: a, value: a ? Math.round(sizes[a] ?? 0) : 0, min: p?.minSize ?? 0, max: p?.maxSize ?? 100 };\n  };\n\n  return (\n    <ResizableContext.Provider value={{ direction, sizes, register, resize, set, limits }}>\n      <div\n        ref={group}\n        data-slot=\"resizable-panel-group\"\n        data-panel-group-direction={direction}\n        className={cn(\"flex h-full w-full\", direction === \"vertical\" && \"flex-col\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </ResizableContext.Provider>\n  );\n}\n\nfunction ResizablePanel({ defaultSize, minSize = 0, maxSize = 100, className, style, id: idProp, ...props }: React.ComponentProps<\"div\"> & { defaultSize?: number; minSize?: number; maxSize?: number }) {\n  const { register, sizes } = useGroup();\n  const auto = React.useId();\n  const id = idProp ?? auto;\n  React.useLayoutEffect(() => register({ id, defaultSize, minSize, maxSize, order: 0 }), [register, id, defaultSize, minSize, maxSize]);\n  const size = sizes[id] ?? defaultSize;\n  return (\n    <div\n      data-slot=\"resizable-panel\"\n      data-panel-id={id}\n      data-panel-size={size === undefined ? undefined : Math.round(size)}\n      className={cn(\"min-h-0 min-w-0 overflow-hidden\", className)}\n      style={{ flexGrow: size ?? 1, flexShrink: 1, flexBasis: 0, ...style }}\n      {...props}\n    />\n  );\n}\n\nfunction ResizableHandle({ withHandle, className, ...props }: React.ComponentProps<\"div\"> & { withHandle?: boolean }) {\n  const { direction, resize, set, limits } = useGroup();\n  const ref = React.useRef<HTMLDivElement>(null);\n  const drag = React.useRef<{ start: number; size: number; pointer: number } | null>(null);\n  const [, force] = React.useReducer((n: number) => n + 1, 0);\n  const vertical = direction === \"vertical\";\n  React.useEffect(() => force(), []);\n  const lim = ref.current ? limits(ref.current) : { value: 0, min: 0, max: 100 };\n\n  return (\n    <div\n      ref={ref}\n      role=\"separator\"\n      tabIndex={0}\n      aria-orientation={vertical ? \"horizontal\" : \"vertical\"}\n      aria-valuenow={lim.value}\n      aria-valuemin={lim.min}\n      aria-valuemax={lim.max}\n      aria-label=\"Resize\"\n      data-slot=\"resizable-handle\"\n      data-panel-group-direction={direction}\n      className={cn(\n        \"relative flex shrink-0 touch-none items-center justify-center bg-line-subtle outline-none select-none after:absolute focus-visible:bg-line-brand focus-visible:outline-none data-[dragging]:bg-line-brand\",\n        vertical ? \"h-px w-full cursor-row-resize after:inset-x-0 after:-inset-y-1.5\" : \"w-px cursor-col-resize after:inset-y-0 after:-inset-x-1.5\",\n        className,\n      )}\n      onPointerDown={(e) => {\n        if (drag.current || e.button !== 0) return;\n        const group = ref.current?.parentElement;\n        if (!group) return;\n        e.currentTarget.setPointerCapture(e.pointerId);\n        e.currentTarget.dataset.dragging = \"\";\n        drag.current = { start: vertical ? e.clientY : e.clientX, size: vertical ? group.clientHeight : group.clientWidth, pointer: e.pointerId };\n      }}\n      onPointerMove={(e) => {\n        const d = drag.current;\n        if (!d || e.pointerId !== d.pointer || !ref.current) return;\n        const pos = vertical ? e.clientY : e.clientX;\n        const delta = ((pos - d.start) / d.size) * 100;\n        if (Math.abs(delta) < 0.1) return;\n        resize(ref.current, delta);\n        d.start = pos;\n      }}\n      onPointerUp={(e) => {\n        drag.current = null;\n        delete e.currentTarget.dataset.dragging;\n      }}\n      onPointerCancel={(e) => {\n        drag.current = null;\n        delete e.currentTarget.dataset.dragging;\n      }}\n      onKeyDown={(e) => {\n        if (!ref.current) return;\n        const step = e.shiftKey ? 10 : 2;\n        const back = vertical ? \"ArrowUp\" : \"ArrowLeft\";\n        const forward = vertical ? \"ArrowDown\" : \"ArrowRight\";\n        if (e.key === back || e.key === forward) {\n          e.preventDefault();\n          resize(ref.current, e.key === back ? -step : step);\n        } else if (e.key === \"Home\" || e.key === \"End\") {\n          e.preventDefault();\n          set(ref.current, e.key === \"Home\" ? \"min\" : \"max\");\n        }\n      }}\n      {...props}\n    >\n      {withHandle && (\n        <div className={cn(\"z-10 flex items-center justify-center rounded-xs border border-line-field bg-surface text-icon-secondary\", vertical ? \"h-3 w-5\" : \"h-5 w-3\")}>\n          <DotsSixVerticalIcon weight=\"bold\" className={cn(\"size-3\", vertical && \"rotate-90\")} />\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport { ResizableHandle, ResizablePanel, ResizablePanelGroup };\n"
    }
  ]
}
