{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"resizable","type":"registry:ui","title":"Resizable","description":"Panels whose sizes you change by dragging the handle between them, or with the keyboard.","dependencies":["lucide-react"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json"],"files":[{"path":"registry/dashral/resizable.tsx","type":"registry:file","target":"components/dashral/resizable.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { GripVerticalIcon } from \"lucide-react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { cn } from \"./lib/utils\"\n\n// Panels whose sizes the user changes by dragging the handle between them, or with the keyboard: focus a handle, then\n// the arrow keys (Shift for bigger steps), Home and End. Follows the WAI-ARIA window splitter pattern. Sizes are\n// percentages of the group. Panels and handles are direct children of the group and alternate — panel, handle, panel —\n// which is how the group tells them apart (checking component types doesn't work across the server/client boundary).\n\ntype Direction = \"horizontal\" | \"vertical\"\ntype PanelSpec = { defaultSize?: number; minSize: number; maxSize: number }\ntype GroupContext = {\n  direction: Direction\n  sizes: number[]\n  specs: PanelSpec[]\n  ids: string[]\n  resize: (handle: number, delta: number) => void\n  setFirst: (handle: number, size: number) => void\n  measure: (handle: number) => { before: number; after: number }\n}\n\nconst Context = React.createContext<GroupContext | null>(null)\nconst useGroup = () => {\n  const context = React.useContext(Context)\n  if (!context) throw new Error(\"ResizablePanel and ResizableHandle go inside a ResizablePanelGroup.\")\n  return context\n}\n\n\n/** Panels without a defaultSize share what the others leave. */\nfunction initialSizes(specs: PanelSpec[]) {\n  const given = specs.reduce((sum, s) => sum + (s.defaultSize ?? 0), 0)\n  const open = specs.filter((s) => s.defaultSize === undefined).length\n  return specs.map((s) => s.defaultSize ?? Math.max(0, 100 - given) / Math.max(open, 1))\n}\n\nfunction ResizablePanelGroup({\n  direction = \"horizontal\",\n  onLayout,\n  className,\n  children,\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"dir\"> & {\n  direction?: Direction\n  /** Called with every panel's size (percent) when a resize ends — store it to restore the layout next time. */\n  onLayout?: (sizes: number[]) => void\n}) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  const baseId = React.useId()\n  const items = React.Children.toArray(children).filter(React.isValidElement)\n  // Even positions are panels, odd ones handles.\n  const panels = items.filter((_, i) => i % 2 === 0) as React.ReactElement<ResizablePanelProps>[]\n  const specs: PanelSpec[] = panels.map((p) => ({\n    defaultSize: p.props.defaultSize,\n    minSize: p.props.minSize ?? 0,\n    maxSize: p.props.maxSize ?? 100,\n  }))\n  const [sizes, setSizes] = React.useState(() => initialSizes(specs))\n  const ids = specs.map((_, i) => `${baseId}-panel-${i}`)\n  const latest = React.useRef(sizes)\n  latest.current = sizes\n\n  /** Moves the boundary after panel `handle` by `delta` percent, within both panels' limits. */\n  function resize(handle: number, delta: number) {\n    setSizes((current) => {\n      const a = specs[handle]!\n      const b = specs[handle + 1]!\n      const pair = current[handle]! + current[handle + 1]!\n      const low = Math.max(a.minSize, pair - b.maxSize)\n      const high = Math.min(a.maxSize, pair - b.minSize)\n      const first = Math.min(high, Math.max(low, current[handle]! + delta))\n      const next = [...current]\n      next[handle] = first\n      next[handle + 1] = pair - first\n      return next\n    })\n  }\n\n  const context: GroupContext = {\n    direction,\n    sizes,\n    specs,\n    ids,\n    resize,\n    setFirst: (handle, size) => resize(handle, size - latest.current[handle]!),\n    measure: (handle) => {\n      const panels = ref.current?.querySelectorAll<HTMLElement>(\":scope > [data-slot=resizable-panel]\")\n      const rect = (i: number) => panels?.[i]?.getBoundingClientRect()\n      const length = (r: DOMRect | undefined) => (r ? (direction === \"horizontal\" ? r.width : r.height) : 0)\n      return { before: length(rect(handle)), after: length(rect(handle + 1)) }\n    },\n  }\n\n  // Tell the page the final layout once a drag or key press is over.\n  const settled = React.useRef(sizes)\n  React.useEffect(() => {\n    const done = () => {\n      if (settled.current !== latest.current) onLayout?.(latest.current)\n      settled.current = latest.current\n    }\n    window.addEventListener(\"pointerup\", done)\n    window.addEventListener(\"keyup\", done)\n    return () => {\n      window.removeEventListener(\"pointerup\", done)\n      window.removeEventListener(\"keyup\", done)\n    }\n  }, [onLayout])\n\n  let panel = 0\n  let handle = 0\n  return (\n    <Context.Provider value={context}>\n      <div\n        ref={ref}\n        data-slot=\"resizable-panel-group\"\n        data-direction={direction}\n        className={cn(\"flex size-full\", direction === \"vertical\" && \"flex-col\", className)}\n        {...props}\n      >\n        {items.map((child, i) =>\n          React.cloneElement(child as React.ReactElement<{ index: number }>, { index: i % 2 === 0 ? panel++ : handle++ })\n        )}\n      </div>\n    </Context.Provider>\n  )\n}\n\ntype ResizablePanelProps = React.ComponentProps<\"div\"> & {\n  /** Starting size in percent of the group. */\n  defaultSize?: number\n  minSize?: number\n  maxSize?: number\n}\n\nfunction ResizablePanel({ className, style, index = 0, defaultSize: _d, minSize: _min, maxSize: _max, ...props }: ResizablePanelProps & { index?: number }) {\n  const { sizes, ids } = useGroup()\n  return (\n    <div\n      data-slot=\"resizable-panel\"\n      id={ids[index]}\n      className={cn(\"min-h-0 min-w-0 overflow-hidden\", className)}\n      style={{ flex: `${sizes[index] ?? 0} 1 0px`, ...style }}\n      {...props}\n    />\n  )\n}\n\ntype ResizableHandleProps = React.ComponentProps<\"div\"> & {\n  /** Show a grip on the handle, so it's easier to find. */\n  withHandle?: boolean\n}\n\nfunction ResizableHandle({ withHandle, className, index = 0, \"aria-label\": ariaLabelProp, ...props }: ResizableHandleProps & { index?: number }) {\n  const { direction, sizes, specs, ids, resize, setFirst, measure } = useGroup()\n  // Where the drag began and the panel's size then, so the size follows the pointer exactly (and catches up after\n  // being held at a limit).\n  const drag = React.useRef<{ start: number; size: number; unitsPerPixel: number } | null>(null)\n  const horizontal = direction === \"horizontal\"\n  const size = sizes[index] ?? 0\n  // Right to left, the first panel is on the right: moving the line left makes it bigger.\n  const { dir, t } = useLocale()\n  const ariaLabel = ariaLabelProp ?? t(\"resize\")\n  const sign = horizontal && dir === \"rtl\" ? -1 : 1\n\n  return (\n    <div\n      data-slot=\"resizable-handle\"\n      role=\"separator\"\n      tabIndex={0}\n      aria-label={ariaLabel}\n      aria-controls={ids[index]}\n      // A horizontal group is split by vertical lines.\n      aria-orientation={horizontal ? \"vertical\" : \"horizontal\"}\n      aria-valuenow={Math.round(size)}\n      aria-valuemin={Math.round(specs[index]?.minSize ?? 0)}\n      aria-valuemax={Math.round(specs[index]?.maxSize ?? 100)}\n      data-direction={direction}\n      className={cn(\n        \"relative flex shrink-0 touch-none items-center justify-center bg-border outline-none select-none focus-visible:ring-2 focus-visible:ring-ring/50\",\n        horizontal\n          ? \"w-px cursor-col-resize after:absolute after:inset-y-0 after:left-1/2 after:w-2 after:-translate-x-1/2\"\n          : \"h-px cursor-row-resize after:absolute after:inset-x-0 after:top-1/2 after:h-2 after:-translate-y-1/2\",\n        \"hover:bg-ring/60 data-[dragging]:bg-ring\",\n        className\n      )}\n      onPointerDown={(event) => {\n        if (event.button !== 0) return\n        event.preventDefault()\n        event.currentTarget.setPointerCapture(event.pointerId)\n        event.currentTarget.setAttribute(\"data-dragging\", \"\")\n        const { before, after } = measure(index)\n        const pair = (sizes[index] ?? 0) + (sizes[index + 1] ?? 0)\n        drag.current = { start: horizontal ? event.clientX : event.clientY, size, unitsPerPixel: before + after ? pair / (before + after) : 0 }\n      }}\n      onPointerMove={(event) => {\n        if (!drag.current) return\n        const position = horizontal ? event.clientX : event.clientY\n        setFirst(index, drag.current.size + sign * (position - drag.current.start) * drag.current.unitsPerPixel)\n      }}\n      onPointerUp={(event) => {\n        drag.current = null\n        event.currentTarget.removeAttribute(\"data-dragging\")\n      }}\n      onKeyDown={(event) => {\n        const step = event.shiftKey ? 10 : 2\n        const back = horizontal ? (sign < 0 ? \"ArrowRight\" : \"ArrowLeft\") : \"ArrowUp\"\n        const forward = horizontal ? (sign < 0 ? \"ArrowLeft\" : \"ArrowRight\") : \"ArrowDown\"\n        if (event.key === back) resize(index, -step)\n        else if (event.key === forward) resize(index, step)\n        else if (event.key === \"Home\") setFirst(index, specs[index]?.minSize ?? 0)\n        else if (event.key === \"End\") setFirst(index, specs[index]?.maxSize ?? 100)\n        else return\n        event.preventDefault()\n      }}\n      {...props}\n    >\n      {withHandle && (\n        <span aria-hidden className={cn(\"z-10 flex items-center justify-center rounded-sm border bg-border\", horizontal ? \"h-4 w-3\" : \"h-3 w-4 rotate-90\")}>\n          <GripVerticalIcon className=\"size-2.5\" />\n        </span>\n      )}\n    </div>\n  )\n}\n\ntype ResizablePanelSpec = {\n  content: React.ReactNode\n  /** Starting size in percent of the group. */\n  defaultSize?: number\n  minSize?: number\n  maxSize?: number\n  className?: string\n}\n\ntype ResizableProps = Omit<React.ComponentProps<\"div\">, \"dir\" | \"children\"> & {\n  /** The panels in order, with a handle between each two. Put another Resizable in a panel to split it the other way. */\n  panels: ResizablePanelSpec[]\n  direction?: Direction\n  /** A grip on each handle, so it's easier to find. */\n  withHandle?: boolean\n  /** Called with every panel's size (percent) when a resize ends — store it to restore the layout next time. */\n  onLayout?: (sizes: number[]) => void\n  classNames?: { panel?: string; handle?: string }\n}\n\n/** Panels split by handles you drag (or move with the arrow keys), in one tag. */\nfunction Resizable({ panels, withHandle, classNames, ...props }: ResizableProps) {\n  return (\n    <ResizablePanelGroup {...props}>\n      {panels.flatMap((panel, i) => [\n        ...(i > 0 ? [<ResizableHandle key={`handle-${i}`} withHandle={withHandle} className={classNames?.handle} />] : []),\n        <ResizablePanel\n          key={`panel-${i}`}\n          defaultSize={panel.defaultSize}\n          minSize={panel.minSize}\n          maxSize={panel.maxSize}\n          className={cn(classNames?.panel, panel.className)}\n        >\n          {panel.content}\n        </ResizablePanel>,\n      ])}\n    </ResizablePanelGroup>\n  )\n}\n\nexport { Resizable, type ResizableProps, type ResizablePanelSpec }\n"}]}