{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "resizable-panels",
  "type": "registry:ui",
  "title": "Resizable panels",
  "description": "Trade space between panes by dragging the divider between them.",
  "categories": [
    "disclosure"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/resizable-panels/resizable-panels.tsx",
      "content": "\"use client\";\n\nimport { Children, Fragment, isValidElement, useEffect, useRef, useState } from \"react\";\nimport type {\n  CSSProperties,\n  KeyboardEvent as ReactKeyboardEvent,\n  PointerEvent as ReactPointerEvent,\n  ReactElement,\n  ReactNode,\n} from \"react\";\nimport { AnimatePresence, animate, motion, motionValue, useReducedMotion, useTransform } from \"motion/react\";\nimport type { AnimationPlaybackControls, MotionValue, TargetAndTransition, Transition } from \"motion/react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\n\n/** One pane of a `ResizablePanels` group. It only carries configuration; the group renders it. */\nexport interface ResizablePanelProps {\n  /** Stable id, also used by the dividers' `aria-controls`. */\n  id: string;\n  /** Names the pane for its divider, for example \"Folders\". */\n  label: string;\n  /** Starting share of the group. Shares are relative, so they need not add up to 100. */\n  defaultSize: number;\n  /** Width in px where the pane starts to resist. Its content never reflows below it; it clips and fades instead. */\n  minSize?: number;\n  maxSize?: number;\n  /** Dragging well past the minimum, or flicking toward the edge, snaps the pane closed. Its divider then shows a restore tab. */\n  collapsible?: boolean;\n  defaultCollapsed?: boolean;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * A horizontal split view whose panes people resize by dragging the dividers between them, the way a mail or code editor window works.\n * Use it when people need to trade space between side-by-side content for the task at hand. Dividers follow the pointer 1:1, resist past\n * a pane's limits, and snap collapsible panes closed. Each divider is a focusable separator: arrows resize, Home and End jump to the limits,\n * Enter hides or restores a collapsible pane, and a double-click resets the layout. The group needs a definite height.\n */\nexport interface ResizablePanelsProps {\n  /** Names the group for assistive technology. */\n  label?: string;\n  /** `ResizablePanel` elements, in order. The set of panes should stay the same for the life of the group. */\n  children: ReactNode;\n  /** Called with each pane's share in percent after a resize settles. */\n  onLayoutChange?: (sizes: number[]) => void;\n  className?: string;\n}\n\ntype Mode = \"open\" | \"before\" | \"after\";\ntype Drag = {\n  handle: number;\n  pointer: number;\n  startX: number;\n  a0: number;\n  b0: number;\n  pair: number;\n  lo: number;\n  hi: number;\n  mode: Mode;\n  startMode: Mode;\n  moved: boolean;\n  catching: boolean;\n  samples: [number, number][];\n};\n\nconst DEFAULT_MIN = 80;\n/** How far in px a pane can be pulled past a limit before it stops giving. */\nconst STRETCH = 44;\n/** Keyboard steps in px; Shift takes the larger one. */\nconst STEP = 16,\n  BIG_STEP = 64;\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n/** Past a limit every pixel costs more, like pulling against elastic. */\nconst rubber = (overshoot: number) => (overshoot * STRETCH * 0.55) / (STRETCH + 0.55 * overshoot);\nconst band = (value: number, lo: number, hi: number) =>\n  value < lo ? lo - rubber(lo - value) : value > hi ? hi + rubber(value - hi) : value;\n/** A collapsible pane snaps closed once the pointer is this far below its minimum. */\nconst collapseAt = (min: number) => min - Math.max(min * 0.5, 40);\n/** Short momentum projection, used only to decide whether a flick collapses or restores a pane. */\nconst project = (velocity: number, rate = 0.99) => ((velocity / 1000) * rate) / (1 - rate);\nconst toShares = (values: number[]) => {\n  const total = values.reduce((sum, value) => sum + value, 0) || 1;\n  return values.map((value) => (value / total) * 100);\n};\n\nconst tabIn: TargetAndTransition = { opacity: 0, scale: 0.6, filter: `blur(${motionTokens.blur.subtle}px)` };\nconst tabRest: TargetAndTransition = { opacity: 1, scale: 1, filter: \"blur(0px)\" };\nconst tabOut: TargetAndTransition = {\n  ...tabIn,\n  transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] },\n};\nconst tabEnter = {\n  ...motionTokens.spring.snappy,\n  opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] },\n  filter: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] },\n} as const;\n\nfunction velocityOf(samples: [number, number][], now: number) {\n  const recent = samples.filter(([time]) => now - time <= 90);\n  const first = recent[0],\n    last = recent[recent.length - 1];\n  if (!first || !last || recent.length < 2 || now - last[0] > 50) return 0;\n  const [firstTime, firstX] = first,\n    [lastTime, lastX] = last;\n  return lastTime > firstTime ? (lastX - firstX) / ((lastTime - firstTime) / 1000) : 0;\n}\n\n/** Carries a pane's configuration into `ResizablePanels`. Rendered on its own it shows its children unchanged. */\nexport function ResizablePanel({ children }: ResizablePanelProps) {\n  return <>{children}</>;\n}\n\nexport function ResizablePanels({ label, children, onLayoutChange, className }: ResizablePanelsProps) {\n  const configs = Children.toArray(children)\n    .filter((child): child is ReactElement<ResizablePanelProps> => isValidElement(child))\n    .map((child) => child.props);\n  const count = configs.length;\n  const reduced = useReducedMotion() ?? false;\n  const startsCollapsed = (config: ResizablePanelProps) => Boolean(config.collapsible && config.defaultCollapsed);\n  // Sizes are flex-grow factors: the default shares before the first interaction, then px. Flex keeps them proportional as the group resizes.\n  const [sizes] = useState(() =>\n    configs.map((config) => motionValue(startsCollapsed(config) ? 0 : config.defaultSize)),\n  );\n  const [fades] = useState(() => configs.map((config) => motionValue(startsCollapsed(config) ? 0 : 1)));\n  const [collapsed, setCollapsed] = useState(() => configs.map(startsCollapsed));\n  const [free, setFree] = useState(() => configs.map(() => false));\n  const [resizing, setResizing] = useState<number | null>(null);\n  const [shares, setShares] = useState(() =>\n    toShares(configs.map((config) => (startsCollapsed(config) ? 0 : config.defaultSize))),\n  );\n  const [available, setAvailable] = useState(0);\n  const groupRef = useRef<HTMLDivElement>(null);\n  const panelRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const restoreSizes = useRef<(number | null)[]>(configs.map(() => null));\n  const running = useRef<AnimationPlaybackControls[]>([]);\n  const token = useRef(0);\n  const goals = useRef<number[] | null>(null);\n  const stale = useRef(true);\n  const drag = useRef<Drag | null>(null);\n  // A group narrower than every minimum together scales the minimums down in proportion, the same floor the CSS applies,\n  // so the panes fill the group instead of overflowing it and a press never re-lays them out.\n  const minTotal = configs.reduce((sum, config) => sum + (config.minSize ?? DEFAULT_MIN), 0);\n  const minOf = (index: number) => {\n    const min = configs[index]?.minSize ?? DEFAULT_MIN;\n    return available > 0 && available < minTotal ? (min * available) / minTotal : min;\n  };\n  const maxOf = (index: number) => configs[index]?.maxSize ?? Infinity;\n\n  // Content fades as a pane is squeezed below its minimum, so a collapsing pane dissolves rather than crushes.\n  const minKey = configs.map((_, index) => minOf(index)).join();\n  useEffect(() => {\n    const mins = minKey.split(\",\").map(Number);\n    const stops = sizes.map((size, index) =>\n      size.on(\"change\", (value) => fades[index]?.set(clamp(value / Math.max(1, mins[index] ?? DEFAULT_MIN), 0, 1))),\n    );\n    return () => stops.forEach((stop) => stop());\n  }, [sizes, fades, minKey]);\n\n  useEffect(() => {\n    const group = groupRef.current;\n    if (!group || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      stale.current = true;\n      setAvailable(panelRefs.current.reduce((sum, node) => sum + (node?.getBoundingClientRect().width ?? 0), 0));\n    });\n    observer.observe(group);\n    return () => observer.disconnect();\n  }, []);\n\n  /** Starts an interaction: stops anything in flight and makes sure the sizes are the px widths on screen, so nothing moves. */\n  function begin(involved: number[]) {\n    token.current += 1;\n    running.current.forEach((controls) => controls.stop());\n    running.current = [];\n    // Only a group resize (or the first interaction) can put the sizes out of step with the screen; otherwise they are the truth,\n    // even before a jump has painted.\n    if (stale.current) {\n      stale.current = false;\n      const widths = panelRefs.current.slice(0, count).map((node) => node?.getBoundingClientRect().width ?? 0);\n      if (widths.some((width, index) => Math.abs(width - sizes[index]!.get()) > 0.5))\n        widths.forEach((width, index) => sizes[index]!.jump(width));\n    }\n    // Freed panes drop their CSS limits while JS owns them, so rubber-banding and collapsing can go past them.\n    if (involved.some((index) => !free[index]))\n      setFree((current) => current.map((value, index) => value || involved.includes(index)));\n    return sizes.map((size) => size.get());\n  }\n\n  function commit() {\n    const values = sizes.map((size) => size.get());\n    setFree(configs.map(() => false));\n    setShares(toShares(values));\n    onLayoutChange?.(toShares(values));\n  }\n\n  /** Springs panes to new widths from wherever they are, keeping their velocity, then hands the limits back to CSS. */\n  function glide(targets: (number | null)[], transition: Transition = motionTokens.spring.smooth) {\n    const mine = token.current;\n    const finals = targets.map((target, index) => target ?? sizes[index]!.get());\n    goals.current = finals;\n    // Assistive technology hears the destination right away rather than after the spring's tail settles.\n    setShares(toShares(finals));\n    const moving = targets.flatMap((target, index) =>\n      target === null || Math.abs(target - sizes[index]!.get()) < 0.01 ? [] : [[index, target] as const],\n    );\n    let pending = moving.length;\n    const done = () => {\n      if (token.current === mine) commit();\n    };\n    if (reduced || !pending) {\n      moving.forEach(([index, target]) => sizes[index]!.jump(target));\n      done();\n      return;\n    }\n    running.current = moving.map(([index, target]) =>\n      animate(sizes[index], target, {\n        ...transition,\n        onComplete: () => {\n          pending -= 1;\n          if (!pending) done();\n        },\n      }),\n    );\n  }\n\n  function limits(handle: number, a: number, b: number) {\n    const pair = a + b;\n    let lo = Math.max(minOf(handle), pair - maxOf(handle + 1)),\n      hi = Math.min(maxOf(handle), pair - minOf(handle + 1));\n    // A group too narrow for every minimum may already hold a pane below it; never yank it to the limit on grab.\n    if (a > 0 && b > 0) {\n      lo = Math.min(lo, a);\n      hi = Math.max(hi, a);\n    }\n    if (lo > hi) lo = hi = clamp((lo + hi) / 2, 0, pair);\n    return { pair, lo: clamp(lo, 0, pair), hi: clamp(hi, 0, pair) };\n  }\n\n  function resolve(handle: number, raw: number, pair: number, lo: number, hi: number): { mode: Mode; size: number } {\n    if (configs[handle]!.collapsible && raw < collapseAt(minOf(handle))) return { mode: \"before\", size: 0 };\n    if (configs[handle + 1]!.collapsible && pair - raw < collapseAt(minOf(handle + 1)))\n      return { mode: \"after\", size: pair };\n    return { mode: \"open\", size: band(raw, lo, hi) };\n  }\n\n  function setCollapsedPair(handle: number, mode: Mode) {\n    setCollapsed((current) =>\n      current.map((value, index) =>\n        index === handle ? mode === \"before\" : index === handle + 1 ? mode === \"after\" : value,\n      ),\n    );\n  }\n\n  function remember(handle: number, a: number, b: number, mode: Mode) {\n    if (mode === \"before\" && a > 0) restoreSizes.current[handle] = a;\n    if (mode === \"after\" && b > 0) restoreSizes.current[handle + 1] = b;\n  }\n\n  function modeOf(handle: number): Mode {\n    return collapsed[handle] ? \"before\" : collapsed[handle + 1] ? \"after\" : \"open\";\n  }\n\n  /** Sets a pane pair to a new boundary. Any change of collapse state springs; everything else moves with the pointer. */\n  function moveBoundary(current: Drag, size: number) {\n    const [a, b] = [sizes[current.handle]!, sizes[current.handle + 1]!];\n    if (current.catching && !reduced) {\n      if (Math.abs(a.get() - size) < 0.75) {\n        current.catching = false;\n        running.current.forEach((controls) => controls.stop());\n        running.current = [];\n      } else {\n        running.current = [\n          animate(a, size, motionTokens.spring.smooth),\n          animate(b, current.pair - size, motionTokens.spring.smooth),\n        ];\n        return;\n      }\n    }\n    a.set(size);\n    b.set(current.pair - size);\n  }\n\n  function onPointerDown(handle: number, event: ReactPointerEvent<HTMLDivElement>) {\n    if (drag.current || (event.pointerType === \"mouse\" && event.button !== 0)) return;\n    const values = begin([handle, handle + 1]);\n    const { pair, lo, hi } = limits(handle, values[handle]!, values[handle + 1]!);\n    event.currentTarget.setPointerCapture(event.pointerId);\n    const mode = modeOf(handle);\n    drag.current = {\n      handle,\n      pointer: event.pointerId,\n      startX: event.clientX,\n      a0: values[handle]!,\n      b0: values[handle + 1]!,\n      pair,\n      lo,\n      hi,\n      mode,\n      startMode: mode,\n      moved: false,\n      catching: false,\n      samples: [[event.timeStamp, event.clientX]],\n    };\n  }\n\n  function onPointerMove(event: ReactPointerEvent<HTMLDivElement>) {\n    const current = drag.current;\n    if (!current || current.pointer !== event.pointerId) return;\n    const dx = event.clientX - current.startX;\n    if (!current.moved) {\n      if (Math.abs(dx) < 2) return;\n      current.moved = true;\n      setResizing(current.handle);\n    }\n    const next = resolve(current.handle, current.a0 + dx, current.pair, current.lo, current.hi);\n    if (next.mode !== current.mode) {\n      current.mode = next.mode;\n      current.catching = true;\n    }\n    moveBoundary(current, next.size);\n    current.samples.push([event.timeStamp, event.clientX]);\n    if (current.samples.length > 16) current.samples.shift();\n  }\n\n  function finish(event: ReactPointerEvent<HTMLDivElement>, cancelled: boolean) {\n    const current = drag.current;\n    if (!current || current.pointer !== event.pointerId) return;\n    drag.current = null;\n    setResizing(null);\n    const { handle, pair, lo, hi } = current;\n    if (!current.moved) {\n      // A press on a divider that hides a pane brings it back.\n      if (current.startMode !== \"open\" && !cancelled) restore(handle, current.startMode);\n      else commit();\n      return;\n    }\n    const raw = current.a0 + event.clientX - current.startX;\n    const velocity = cancelled ? 0 : velocityOf(current.samples, event.timeStamp);\n    const flung = resolve(handle, raw + project(velocity), pair, lo, hi);\n    // A flick toward an edge collapses; a flick back out restores. Otherwise the divider stays where it was let go, inside its limits.\n    const mode = cancelled ? current.startMode : flung.mode !== \"open\" ? flung.mode : \"open\";\n    const size = mode === \"before\" ? 0 : mode === \"after\" ? pair : cancelled ? current.a0 : clamp(raw, lo, hi);\n    remember(handle, current.a0, current.b0, mode);\n    setCollapsedPair(handle, mode);\n    glide(sizes.map((_, index) => (index === handle ? size : index === handle + 1 ? pair - size : null)));\n  }\n\n  function restore(handle: number, side: Mode) {\n    const values = begin([handle, handle + 1]);\n    const { pair, lo, hi } = limits(handle, values[handle]!, values[handle + 1]!);\n    const size =\n      side === \"before\"\n        ? clamp(restoreSizes.current[handle] ?? minOf(handle), lo, hi)\n        : clamp(pair - (restoreSizes.current[handle + 1] ?? minOf(handle + 1)), lo, hi);\n    setCollapsedPair(handle, \"open\");\n    glide(sizes.map((_, index) => (index === handle ? size : index === handle + 1 ? pair - size : null)));\n  }\n\n  function collapse(handle: number, side: Mode) {\n    const values = begin([handle, handle + 1]);\n    const pair = values[handle]! + values[handle + 1]!;\n    remember(handle, values[handle]!, values[handle + 1]!, side);\n    setCollapsedPair(handle, side);\n    glide(\n      sizes.map((_, index) =>\n        index === handle\n          ? side === \"before\"\n            ? 0\n            : pair\n          : index === handle + 1\n            ? side === \"before\"\n              ? pair\n              : 0\n            : null,\n      ),\n    );\n  }\n\n  /** Resets to the default shares, fitted to the current width so every pane respects its limits. */\n  function reset() {\n    const values = begin(configs.map((_, index) => index));\n    const total = values.reduce((sum, value) => sum + value, 0);\n    const wanted = configs.map((config) => (startsCollapsed(config) ? 0 : config.defaultSize));\n    const targets = [...wanted];\n    const frozen = wanted.map((value) => value === 0);\n    for (let pass = 0; pass < count; pass++) {\n      const fixed = targets.reduce((sum, value, index) => (frozen[index] ? sum + value : sum), 0);\n      const share = wanted.reduce((sum, value, index) => (frozen[index] ? sum : sum + value), 0);\n      let violated = false;\n      targets.forEach((_, index) => {\n        if (frozen[index]) return;\n        const size = share ? (wanted[index]! / share) * (total - fixed) : 0;\n        const bounded = clamp(size, minOf(index), maxOf(index));\n        targets[index] = size;\n        if (bounded !== size) {\n          targets[index] = bounded;\n          frozen[index] = true;\n          violated = true;\n        }\n      });\n      if (!violated) break;\n    }\n    setCollapsed(configs.map(startsCollapsed));\n    glide(targets);\n  }\n\n  function onKeyDown(handle: number, event: ReactKeyboardEvent<HTMLDivElement>) {\n    const current = drag.current;\n    if (event.key === \"Escape\" && current) {\n      // Escape during a drag puts the divider back where it started.\n      event.preventDefault();\n      drag.current = null;\n      setResizing(null);\n      setCollapsedPair(handle, current.startMode);\n      glide(sizes.map((_, index) => (index === handle ? current.a0 : index === handle + 1 ? current.b0 : null)));\n      return;\n    }\n    if (current || ![\"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\", \"Enter\"].includes(event.key)) return;\n    event.preventDefault();\n    const mode = modeOf(handle);\n    const canCollapseBefore = Boolean(configs[handle]!.collapsible),\n      canCollapseAfter = Boolean(configs[handle + 1]!.collapsible);\n    if (event.key === \"Enter\") {\n      if (mode !== \"open\") restore(handle, mode);\n      else if (canCollapseBefore) collapse(handle, \"before\");\n      else if (canCollapseAfter) collapse(handle, \"after\");\n      return;\n    }\n    // Repeated presses build on where the divider is heading, not on where the spring happens to be this frame.\n    const heading =\n      goals.current && (sizes[handle]!.isAnimating() || sizes[handle + 1]!.isAnimating()) ? goals.current : null;\n    const values = begin([handle, handle + 1]);\n    const pair = values[handle]! + values[handle + 1]!;\n    const a = heading ? clamp(heading[handle]!, 0, pair) : values[handle]!,\n      step = event.shiftKey ? BIG_STEP : STEP;\n    const { lo, hi } = limits(handle, a, pair - a);\n    // Arrows walk out of a collapsed pane at its minimum, and past a limit into collapse when the pane allows it.\n    if (event.key === \"ArrowLeft\" && mode === \"after\") return restoreTo(handle, hi, pair);\n    if (event.key === \"ArrowRight\" && mode === \"before\") return restoreTo(handle, lo, pair);\n    if (event.key === \"ArrowLeft\" && a <= lo + 0.5 && canCollapseBefore && mode === \"open\")\n      return collapse(handle, \"before\");\n    if (event.key === \"ArrowRight\" && a >= hi - 0.5 && canCollapseAfter && mode === \"open\")\n      return collapse(handle, \"after\");\n    if (mode !== \"open\") return;\n    const size =\n      event.key === \"Home\"\n        ? lo\n        : event.key === \"End\"\n          ? hi\n          : clamp(a + (event.key === \"ArrowLeft\" ? -step : step), lo, hi);\n    glide(\n      sizes.map((_, index) => (index === handle ? size : index === handle + 1 ? pair - size : null)),\n      event.key.startsWith(\"Arrow\") ? motionTokens.spring.snappy : motionTokens.spring.smooth,\n    );\n  }\n\n  function restoreTo(handle: number, size: number, pair: number) {\n    setCollapsedPair(handle, \"open\");\n    glide(sizes.map((_, index) => (index === handle ? size : index === handle + 1 ? pair - size : null)));\n  }\n\n  // Panes share the width through flex-grow factors, so the layout is right on the server and stays proportional as the group resizes.\n  // A size container, so each pane's floor can scale with the group when it is too narrow for every minimum (see --panel-min).\n  return (\n    <div\n      ref={groupRef}\n      className={cn(\n        \"group/panels @container flex size-full min-h-0 min-w-0 overflow-hidden data-resizing:cursor-col-resize data-resizing:select-none\",\n        className,\n      )}\n      role=\"group\"\n      aria-label={label}\n      data-resizing={resizing !== null || undefined}\n    >\n      {configs.map((config, index) => (\n        <Fragment key={config.id}>\n          {index > 0 && (\n            <Handle\n              before={configs[index - 1]!}\n              after={config}\n              share={shares[index - 1]!}\n              available={available}\n              limits={\n                available\n                  ? limits(index - 1, (shares[index - 1]! / 100) * available, (shares[index]! / 100) * available)\n                  : null\n              }\n              mode={collapsed[index - 1] ? \"before\" : collapsed[index] ? \"after\" : \"open\"}\n              active={resizing === index - 1}\n              reduced={reduced}\n              onPointerDown={(event) => onPointerDown(index - 1, event)}\n              onPointerMove={onPointerMove}\n              onPointerUp={(event) => finish(event, false)}\n              onPointerCancel={(event) => finish(event, true)}\n              onKeyDown={(event) => onKeyDown(index - 1, event)}\n              onDoubleClick={reset}\n            />\n          )}\n          <Pane\n            config={config}\n            size={sizes[index]!}\n            fade={fades[index]!}\n            collapsed={collapsed[index]!}\n            free={free[index]!}\n            anchor={index === 0 && count > 1 ? \"end\" : \"start\"}\n            minTotal={minTotal}\n            gaps={count - 1}\n            paneRef={(node) => {\n              panelRefs.current[index] = node;\n            }}\n          />\n        </Fragment>\n      ))}\n    </div>\n  );\n}\n\nfunction Pane({\n  config,\n  size,\n  fade,\n  collapsed,\n  free,\n  anchor,\n  minTotal,\n  gaps,\n  paneRef,\n}: {\n  config: ResizablePanelProps;\n  size: MotionValue<number>;\n  fade: MotionValue<number>;\n  collapsed: boolean;\n  free: boolean;\n  anchor: \"start\" | \"end\";\n  minTotal: number;\n  gaps: number;\n  paneRef: (node: HTMLDivElement | null) => void;\n}) {\n  const grow = useTransform(size, (value) => Math.max(0, value));\n  const min = config.minSize ?? DEFAULT_MIN;\n  // The group is a size container: below the sum of the minimums, each floor becomes its share of the room between the dividers.\n  const style = {\n    flexGrow: grow,\n    \"--panel-min\": `min(${min}px, (100cqw - ${gaps}px) * ${+(min / Math.max(1, minTotal)).toFixed(5)})`,\n    \"--panel-max\": config.maxSize ? `${config.maxSize}px` : \"none\",\n  } as unknown as CSSProperties;\n  // The content stays anchored to the moving edge, so a closing pane slides away like a drawer instead of being crushed.\n  // CSS enforces each pane's limits until an interaction frees it; JS hands them back once the pane settles inside them.\n  const released = free || collapsed;\n  return (\n    <motion.div\n      ref={paneRef}\n      id={config.id}\n      className={cn(\n        \"relative flex min-h-0 flex-[0_1_0px] overflow-hidden\",\n        released ? \"max-w-none min-w-0\" : \"max-w-(--panel-max) min-w-(--panel-min)\",\n        anchor === \"end\" && \"justify-end\",\n        config.className,\n      )}\n      style={style}\n      data-collapsed={collapsed || undefined}\n      data-free={free || undefined}\n      data-anchor={anchor}\n    >\n      {/* Content ignores the pointer while a divider moves, so hover states never flicker underneath it. */}\n      <motion.div\n        className={cn(\n          \"flex min-h-0 w-full flex-none flex-col group-data-resizing/panels:pointer-events-none\",\n          released && \"min-w-(--panel-min)\",\n        )}\n        style={{ opacity: fade }}\n        inert={collapsed}\n      >\n        {config.children}\n      </motion.div>\n    </motion.div>\n  );\n}\n\ntype HandleProps = {\n  before: ResizablePanelProps;\n  after: ResizablePanelProps;\n  share: number;\n  available: number;\n  limits: { lo: number; hi: number } | null;\n  mode: Mode;\n  active: boolean;\n  reduced: boolean;\n  onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;\n  onPointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;\n  onPointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;\n  onPointerCancel: (event: ReactPointerEvent<HTMLDivElement>) => void;\n  onKeyDown: (event: ReactKeyboardEvent<HTMLDivElement>) => void;\n  onDoubleClick: () => void;\n};\n\n/** The divider is a one pixel line with a 13px grab area (a wider one beside a hidden pane), and a grip that forms while it is hovered, focused, or dragged. */\nconst handle =\n  \"group/handle relative z-2 flex-[0_0_1px] touch-none outline-none [-webkit-tap-highlight-color:transparent] transition-[background-color] duration-fast ease-standard before:absolute before:inset-y-0 before:-inset-x-1.5 before:content-[''] motion-reduce:transition-none\";\nconst grip =\n  \"pointer-events-none absolute top-1/2 left-1/2 h-8 w-1 rounded-full opacity-0 [transform:translate(-50%,-50%)_scaleY(.6)] [transition:opacity_var(--duration-fast)_var(--ease-standard),background-color_var(--duration-fast)_var(--ease-standard),transform_var(--duration-spring)_var(--ease-spring)] motion-reduce:transition-none\";\n/** Once a neighbour is hidden, a tab on the divider points the way it will come back. */\nconst restore =\n  \"pointer-events-none absolute top-1/2 -mt-5 grid h-10 w-[18px] place-items-center rounded-full border border-line bg-surface text-ink-2 shadow-resting transition-[border-color,color] duration-fast ease-standard group-hover/handle:border-line-strong group-hover/handle:text-ink group-focus-visible/handle:border-line-strong group-focus-visible/handle:text-ink motion-reduce:transition-none\";\n\n/** A one pixel divider with a wider grab area. It shows a grip while hovered or dragged, and a restore tab once a neighbouring pane is hidden. */\nfunction Handle({\n  before,\n  after,\n  share,\n  available,\n  limits,\n  mode,\n  active,\n  reduced,\n  onPointerDown,\n  onPointerMove,\n  onPointerUp,\n  onPointerCancel,\n  onKeyDown,\n  onDoubleClick,\n}: HandleProps) {\n  const now = Math.round(share);\n  const min = limits && available ? Math.round((limits.lo / available) * 100) : 0;\n  const max = limits && available ? Math.round((limits.hi / available) * 100) : 100;\n  const hidden = mode === \"before\" ? before.label : mode === \"after\" ? after.label : null;\n  const open = mode === \"open\";\n  return (\n    <div\n      role=\"separator\"\n      tabIndex={0}\n      aria-orientation=\"vertical\"\n      aria-controls={before.id}\n      aria-label={before.label}\n      aria-valuenow={now}\n      aria-valuemin={Math.min(min, now)}\n      aria-valuemax={Math.max(max, now)}\n      aria-valuetext={hidden ? `${hidden} hidden, press Enter to show` : `${now}%`}\n      className={cn(\n        handle,\n        active ? \"bg-line-strong\" : open ? \"bg-line hover:bg-line-strong\" : \"bg-transparent\",\n        open ? \"cursor-col-resize\" : \"cursor-pointer before:-inset-x-[30px]\",\n        mode === \"before\" && \"before:left-0\",\n        mode === \"after\" && \"before:right-0\",\n      )}\n      data-collapsed={mode === \"open\" ? undefined : mode}\n      data-active={active || undefined}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerUp}\n      onPointerCancel={onPointerCancel}\n      onKeyDown={onKeyDown}\n      onDoubleClick={onDoubleClick}\n    >\n      <span\n        className={cn(\n          grip,\n          active ? \"bg-ink opacity-100 [transform:translate(-50%,-50%)_scaleY(1)]\" : \"bg-line-strong\",\n          open &&\n            \"group-hover/handle:opacity-100 group-hover/handle:[transform:translate(-50%,-50%)_scaleY(1)] group-focus-visible/handle:opacity-100 group-focus-visible/handle:[transform:translate(-50%,-50%)_scaleY(1)]\",\n        )}\n        aria-hidden=\"true\"\n      />\n      <AnimatePresence initial={false}>\n        {mode !== \"open\" && (\n          <motion.span\n            key={mode}\n            className={cn(restore, mode === \"before\" ? \"left-[5px] origin-[0_50%]\" : \"right-[5px] origin-[100%_50%]\")}\n            data-side={mode}\n            aria-hidden=\"true\"\n            initial={reduced ? { opacity: 0 } : tabIn}\n            animate={tabRest}\n            exit={reduced ? { opacity: 0, transition: { duration: motionTokens.duration.instant } } : tabOut}\n            transition={reduced ? { duration: motionTokens.duration.instant } : tabEnter}\n          >\n            {mode === \"before\" ? (\n              <ChevronRight size={14} strokeWidth={1.75} />\n            ) : (\n              <ChevronLeft size={14} strokeWidth={1.75} />\n            )}\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport default ResizablePanels;\n",
      "type": "registry:component",
      "target": "@components/iris/resizable-panels/resizable-panels.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/resizable-panels",
  "meta": {
    "kind": "component",
    "group": "Overlays & disclosure",
    "export": "ResizablePanels",
    "tags": [
      "split view",
      "resizable",
      "panes"
    ],
    "docs": "https://irisui.dev/components/resizable-panels"
  }
}
