{
  "$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://mcp.xzero.co.uk/r/xzero-motion-tokens.json"
  ],
  "files": [
    {
      "path": "registry/components/resizable-panels/resizable-panels.tsx",
      "type": "registry:component",
      "target": "@components/xzero/resizable-panels/resizable-panels.tsx",
      "content": "\"use client\";\n\nimport { Children, Fragment, isValidElement, useEffect, useRef, useState } from \"react\";\nimport type { CSSProperties, KeyboardEvent as ReactKeyboardEvent, PointerEvent as ReactPointerEvent, ReactElement, ReactNode } 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 styles from \"./resizable-panels.module.css\";\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 = { handle: number; pointer: number; startX: number; a0: number; b0: number; pair: number; lo: number; hi: number; mode: Mode; startMode: Mode; moved: boolean; catching: boolean; samples: [number, number][] };\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, 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 * .55 / (STRETCH + .55 * overshoot);\nconst band = (value: number, lo: number, hi: number) => 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 * .5, 40);\n/** Short momentum projection, used only to decide whether a flick collapses or restores a pane. */\nconst project = (velocity: number, rate = .99) => velocity / 1000 * rate / (1 - rate);\nconst toShares = (values: number[]) => { const total = values.reduce((sum, value) => sum + value, 0) || 1; return values.map(value => value / total * 100); };\n\nconst tabIn: TargetAndTransition = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };\nconst tabRest: TargetAndTransition = { opacity: 1, scale: 1, filter: \"blur(0px)\" };\nconst tabOut: TargetAndTransition = { ...tabIn, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };\nconst tabEnter = { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] }, filter: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } } as const;\n\nfunction velocityOf(samples: [number, number][], now: number) {\n  const recent = samples.filter(([time]) => now - time <= 90);\n  const first = recent[0], last = recent[recent.length - 1];\n  if (!first || !last || recent.length < 2 || now - last[0] > 50) return 0;\n  const [firstTime, firstX] = first, [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).filter((child): child is ReactElement<ResizablePanelProps> => isValidElement(child)).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(() => configs.map(config => motionValue(startsCollapsed(config) ? 0 : config.defaultSize)));\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(() => toShares(configs.map(config => startsCollapsed(config) ? 0 : config.defaultSize)));\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) => { const min = configs[index]?.minSize ?? DEFAULT_MIN; return available > 0 && available < minTotal ? min * available / minTotal : min; };\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) => size.on(\"change\", value => fades[index]?.set(clamp(value / Math.max(1, mins[index] ?? DEFAULT_MIN), 0, 1))));\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()) > .5)) 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])) 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) => target === null || Math.abs(target - sizes[index]!.get()) < .01 ? [] : [[index, target] as const]);\n    let pending = moving.length;\n    const done = () => { if (token.current === mine) commit(); };\n    if (reduced || !pending) { moving.forEach(([index, target]) => sizes[index]!.jump(target)); done(); return; }\n    running.current = moving.map(([index, target]) => animate(sizes[index], target, { ...transition, onComplete: () => { pending -= 1; if (!pending) done(); } }));\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)), 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) { lo = Math.min(lo, a); hi = Math.max(hi, a); }\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))) 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 => current.map((value, index) => index === handle ? mode === \"before\" : index === handle + 1 ? mode === \"after\" : value));\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) < .75) { current.catching = false; running.current.forEach(controls => controls.stop()); running.current = []; }\n      else { running.current = [animate(a, size, motionTokens.spring.smooth), animate(b, current.pair - size, motionTokens.spring.smooth)]; return; }\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 = { handle, pointer: event.pointerId, startX: event.clientX, a0: values[handle]!, b0: values[handle + 1]!, pair, lo, hi, mode, startMode: mode, moved: false, catching: false, samples: [[event.timeStamp, event.clientX]] };\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) { if (Math.abs(dx) < 2) return; current.moved = true; setResizing(current.handle); }\n    const next = resolve(current.handle, current.a0 + dx, current.pair, current.lo, current.hi);\n    if (next.mode !== current.mode) { current.mode = next.mode; current.catching = true; }\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 = side === \"before\" ? clamp(restoreSizes.current[handle] ?? minOf(handle), lo, hi) : 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(sizes.map((_, index) => index === handle ? (side === \"before\" ? 0 : pair) : index === handle + 1 ? (side === \"before\" ? pair : 0) : null));\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) { targets[index] = bounded; frozen[index] = true; violated = true; }\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), 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 = 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]!, 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 + .5 && canCollapseBefore && mode === \"open\") return collapse(handle, \"before\");\n    if (event.key === \"ArrowRight\" && a >= hi - .5 && canCollapseAfter && mode === \"open\") return collapse(handle, \"after\");\n    if (mode !== \"open\") return;\n    const size = event.key === \"Home\" ? lo : event.key === \"End\" ? hi : clamp(a + (event.key === \"ArrowLeft\" ? -step : step), lo, hi);\n    glide(sizes.map((_, index) => index === handle ? size : index === handle + 1 ? pair - size : null), event.key.startsWith(\"Arrow\") ? motionTokens.spring.snappy : motionTokens.spring.smooth);\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  return <div ref={groupRef} className={[styles.group, className].filter(Boolean).join(\" \")} role=\"group\" aria-label={label} data-resizing={resizing !== null || undefined}>\n    {configs.map((config, index) => <Fragment key={config.id}>\n      {index > 0 && <Handle before={configs[index - 1]!} after={config} share={shares[index - 1]!} available={available} limits={available ? limits(index - 1, shares[index - 1]! / 100 * available, shares[index]! / 100 * available) : null}\n        mode={collapsed[index - 1] ? \"before\" : collapsed[index] ? \"after\" : \"open\"} active={resizing === index - 1} reduced={reduced}\n        onPointerDown={event => onPointerDown(index - 1, event)} onPointerMove={onPointerMove} onPointerUp={event => finish(event, false)} onPointerCancel={event => finish(event, true)}\n        onKeyDown={event => onKeyDown(index - 1, event)} onDoubleClick={reset} />}\n      <Pane config={config} size={sizes[index]!} fade={fades[index]!} collapsed={collapsed[index]!} free={free[index]!} anchor={index === 0 && count > 1 ? \"end\" : \"start\"} minTotal={minTotal} gaps={count - 1} paneRef={node => { panelRefs.current[index] = node; }} />\n    </Fragment>)}\n  </div>;\n}\n\nfunction Pane({ config, size, fade, collapsed, free, anchor, minTotal, gaps, paneRef }: { config: ResizablePanelProps; size: MotionValue<number>; fade: MotionValue<number>; collapsed: boolean; free: boolean; anchor: \"start\" | \"end\"; minTotal: number; gaps: number; paneRef: (node: HTMLDivElement | null) => void }) {\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 = { flexGrow: grow, \"--panel-min\": `min(${min}px, (100cqw - ${gaps}px) * ${+(min / Math.max(1, minTotal)).toFixed(5)})`, \"--panel-max\": config.maxSize ? `${config.maxSize}px` : \"none\" } 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  return <motion.div ref={paneRef} id={config.id} className={[styles.panel, config.className].filter(Boolean).join(\" \")} style={style} data-collapsed={collapsed || undefined} data-free={free || undefined} data-anchor={anchor}>\n    <motion.div className={styles.content} style={{ opacity: fade }} inert={collapsed}>{config.children}</motion.div>\n  </motion.div>;\n}\n\ntype HandleProps = {\n  before: ResizablePanelProps; after: ResizablePanelProps; share: number; available: number; limits: { lo: number; hi: number } | null; mode: Mode; active: boolean; reduced: boolean;\n  onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void; onPointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void; onPointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;\n  onPointerCancel: (event: ReactPointerEvent<HTMLDivElement>) => void; onKeyDown: (event: ReactKeyboardEvent<HTMLDivElement>) => void; onDoubleClick: () => void;\n};\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({ before, after, share, available, limits, mode, active, reduced, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onKeyDown, onDoubleClick }: 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  return <div role=\"separator\" tabIndex={0} aria-orientation=\"vertical\" aria-controls={before.id} aria-label={before.label} aria-valuenow={now} aria-valuemin={Math.min(min, now)} aria-valuemax={Math.max(max, now)}\n    aria-valuetext={hidden ? `${hidden} hidden, press Enter to show` : `${now}%`} className={styles.handle} data-collapsed={mode === \"open\" ? undefined : mode} data-active={active || undefined}\n    onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerCancel={onPointerCancel} onKeyDown={onKeyDown} onDoubleClick={onDoubleClick}>\n    <span className={styles.grip} aria-hidden=\"true\" />\n    <AnimatePresence initial={false}>\n      {mode !== \"open\" && <motion.span key={mode} className={styles.restore} data-side={mode} aria-hidden=\"true\" initial={reduced ? { opacity: 0 } : tabIn} animate={tabRest} exit={reduced ? { opacity: 0, transition: { duration: motionTokens.duration.instant } } : tabOut} transition={reduced ? { duration: motionTokens.duration.instant } : tabEnter}>\n        {mode === \"before\" ? <ChevronRight size={14} strokeWidth={1.75} /> : <ChevronLeft size={14} strokeWidth={1.75} />}\n      </motion.span>}\n    </AnimatePresence>\n  </div>;\n}\n\nexport default ResizablePanels;\n"
    },
    {
      "path": "registry/components/resizable-panels/resizable-panels.module.css",
      "type": "registry:component",
      "target": "@components/xzero/resizable-panels/resizable-panels.module.css",
      "content": "/* 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.group { display: flex; width: 100%; height: 100%; min-width: 0; min-height: 0; overflow: hidden; container-type: inline-size; }\n.group[data-resizing] { cursor: col-resize; user-select: none; -webkit-user-select: none; }\n/* Content ignores the pointer while a divider moves, so hover states never flicker underneath it. */\n.group[data-resizing] .content { pointer-events: none; }\n\n/* CSS enforces each pane's limits until an interaction frees it; JS hands them back once the pane settles inside them. */\n.panel { position: relative; display: flex; min-width: var(--panel-min); max-width: var(--panel-max); min-height: 0; flex: 0 1 0px; overflow: hidden; }\n.panel[data-anchor=\"end\"] { justify-content: flex-end; }\n.panel[data-free], .panel[data-collapsed] { min-width: 0; max-width: none; }\n.content { display: flex; width: 100%; flex: none; flex-direction: column; min-height: 0; }\n.panel[data-free] .content, .panel[data-collapsed] .content { min-width: var(--panel-min); }\n\n/* The divider is a one pixel line with a 13px grab area, and a grip that forms while it is hovered, focused, or dragged. */\n.handle { position: relative; z-index: 2; flex: 0 0 1px; background: var(--border); cursor: col-resize; touch-action: none; outline: none; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard); }\n.handle::before { position: absolute; inset: 0 -6px; content: \"\"; }\n.handle[data-collapsed] { background: transparent; cursor: pointer; }\n.grip { position: absolute; top: 50%; left: 50%; width: 4px; height: 32px; border-radius: var(--radius-pill); background: var(--border-strong); opacity: 0; pointer-events: none; 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); }\n@media (hover: hover) and (pointer: fine) {\n  .handle:not([data-collapsed]):hover { background: var(--border-strong); }\n  .handle:not([data-collapsed]):hover .grip { opacity: 1; transform: translate(-50%, -50%) scaleY(1); }\n  .handle[data-collapsed]:hover .restore { border-color: var(--border-strong); color: var(--foreground); }\n}\n.handle[data-active] { background: var(--border-strong); }\n.handle[data-active] .grip { background: var(--foreground); opacity: 1; transform: translate(-50%, -50%) scaleY(1); }\n.handle:focus-visible:not([data-collapsed]) .grip { opacity: 1; transform: translate(-50%, -50%) scaleY(1); }\n.handle[data-collapsed]:focus-visible .restore { border-color: var(--border-strong); color: var(--foreground); }\n\n/* Once a neighbour is hidden, a tab on the divider points the way it will come back. */\n.restore { position: absolute; top: 50%; display: grid; width: 18px; height: 40px; margin-top: -20px; place-items: center; border: 1px solid var(--border); border-radius: var(--radius-pill); background: var(--surface); box-shadow: var(--shadow-resting); color: var(--text-secondary); pointer-events: none; transition: border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }\n.restore[data-side=\"before\"] { left: 5px; transform-origin: 0 50%; }\n.restore[data-side=\"after\"] { right: 5px; transform-origin: 100% 50%; }\n.handle[data-collapsed]::before { inset: 0 -30px; }\n.handle[data-collapsed=\"before\"]::before { left: 0; }\n.handle[data-collapsed=\"after\"]::before { right: 0; }\n\n@media (prefers-reduced-motion: reduce) {\n  .handle, .grip, .restore { transition: none; }\n}\n"
    }
  ],
  "docs": "Docs and live preview: https://ui.xzero.co.uk/components/resizable-panels",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "split view",
      "resizable",
      "panes"
    ],
    "docs": "https://ui.xzero.co.uk/components/resizable-panels",
    "markdown": "https://ui.xzero.co.uk/components/resizable-panels/markdown"
  }
}