{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"resizable-panels","type":"registry:component","title":"Resizable panels","description":"Panes that trade space across draggable dividers; each divider is a focusable separator with arrow keys, limits, folding for collapsible panes and double-click to restore.","categories":["layout"],"dependencies":["@radix-ui/react-direction@1.1.4","clsx@^2.1.1","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/resizable-panels/messages.ts","type":"registry:file","target":"src/ui/beamline/components/resizable-panels/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  resize: \"Resize {label}\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/resizable-panels/resizable-panels.module.css","type":"registry:file","target":"src/ui/beamline/components/resizable-panels/resizable-panels.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A pane's share glides when it changes by itself (a key step, a fold, a double-click restore, the room folding a\n     pane away or giving it back); under the pointer it follows at once. */\n  .panel {\n    transition: flex-grow var(--pui-duration-standard) var(--pui-ease-default);\n  }\n  [data-dragging] > .panel {\n    transition: none;\n  }\n  /* A divider is a 1px seam (line-soft, like every seam inside one frame) with a grip at its middle that is visible at\n     rest, so the split is discoverable without a pointer. Hover and drag light the seam in the accent; keyboard focus\n     draws the look's focus ring round the grip. The hit area is wider than the line. */\n  .divider {\n    position: relative;\n    z-index: 1;\n    flex: none;\n    /* dragged along one axis; the page keeps the other (a column divider leaves vertical scrolling to it) */\n    touch-action: pan-y;\n    background: var(--pui-line-soft);\n    outline: none;\n  }\n  .divider[aria-orientation=\"vertical\"] {\n    width: 1px;\n    cursor: col-resize;\n  }\n  .divider[aria-orientation=\"horizontal\"] {\n    touch-action: pan-x;\n    height: 1px;\n    cursor: row-resize;\n  }\n  .divider::before {\n    content: \"\";\n    position: absolute;\n    inset: 0 -6px;\n  }\n  .divider[aria-orientation=\"horizontal\"]::before {\n    inset: -6px 0;\n  }\n  /* the lit seam: 3px of accent over the hairline */\n  .divider::after {\n    content: \"\";\n    position: absolute;\n    inset-block: 0;\n    inset-inline-start: -1px;\n    width: 3px;\n    border-radius: var(--pui-radius-pill);\n    background: var(--pui-accent);\n    opacity: 0;\n    transition: opacity var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .divider[aria-orientation=\"horizontal\"]::after {\n    inset-inline: 0;\n    inset-block-start: -1px;\n    width: auto;\n    height: 3px;\n  }\n  .grip {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    width: 4px;\n    height: 28px;\n    translate: -50% -50%;\n    border-radius: var(--pui-radius-pill);\n    background: var(--pui-line-strong);\n    transition: background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .divider[aria-orientation=\"horizontal\"] .grip {\n    width: 28px;\n    height: 4px;\n  }\n  .divider:hover::after,\n  .divider:active::after {\n    opacity: 1;\n    transition-duration: 0ms;\n  }\n  .divider:hover .grip,\n  .divider:active .grip {\n    background: var(--pui-accent);\n    transition-duration: 0ms;\n  }\n  .divider:focus-visible .grip {\n    background: var(--pui-accent);\n    outline: var(--pui-focus-width) var(--pui-focus-style) var(--pui-focus-ring);\n    outline-offset: 2px;\n  }\n  /* a folded pane leaves only its seam and grip, which still unfolds it */\n  .divider[data-folded] .grip {\n    background: var(--pui-ink-3);\n  }\n}\n"},{"path":"beamline/components/resizable-panels/resizable-panels.tsx","type":"registry:file","target":"src/ui/beamline/components/resizable-panels/resizable-panels.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { useDrag } from \"../../hooks/use-drag\";\nimport { english as englishMessages } from \"./messages\";\nimport {\n  Children,\n  Fragment,\n  isValidElement,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type Ref,\n  type KeyboardEvent,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./resizable-panels.module.css\";\n\nexport interface ResizablePanelProps {\n  ref?: Ref<HTMLDivElement>;\n  style?: CSSProperties;\n  id: string;\n  /** The pane's accessible name, also used by its divider: \"Resize Editor\". */\n  label: string;\n  /** Share of the space at first, in percent. Shares are normalised to 100. */\n  defaultSize?: number;\n  /** Smallest size in px. Default 80. */\n  minSize?: number;\n  /** Largest size in px. */\n  maxSize?: number;\n  /** Dragging well past the minimum (or Enter on its divider) folds it away. */\n  collapsible?: boolean;\n  className?: string;\n  children: ReactNode;\n}\n\n/** One pane. It only carries settings; ResizablePanels lays it out. */\nexport function ResizablePanel({ children }: ResizablePanelProps) {\n  return <>{children}</>;\n}\n\nexport type ResizablePanelsClassNames = Partial<Record<\"root\" | \"panel\" | \"divider\" | \"grip\", string>>;\nexport interface ResizablePanelsProps {\n  ref?: Ref<HTMLDivElement>;\n  style?: CSSProperties;\n  classNames?: ResizablePanelsClassNames;\n  /** The split's name, e.g. \"Mail\". */\n  label: string;\n  direction?: \"horizontal\" | \"vertical\";\n  /** Sizes in percent after each change (drag end, key press). */\n  onLayoutChange?: (sizes: number[]) => void;\n  /** Remember sizes in localStorage under this key. */\n  storageKey?: string;\n  className?: string;\n  children: ReactNode;\n}\n\nconst STEP = 5;\n\n/**\n * Panes that trade space across draggable dividers. A divider is a focusable separator: ← → (↑ ↓ when stacked) move it\n * by 5%, Home and End go to the limits, Enter folds a collapsible pane, a double-click restores the starting sizes.\n */\nexport function ResizablePanels({\n  label,\n  direction = \"horizontal\",\n  onLayoutChange,\n  storageKey,\n  className,\n  classNames,\n  style,\n  ref,\n  children,\n}: ResizablePanelsProps) {\n  const i18n = useMessages(\"resizable-panels\", englishMessages);\n\n  const panels = Children.toArray(children).filter((c): c is ReactElement<ResizablePanelProps> => isValidElement(c));\n  const defaults = (() => {\n    const raw = panels.map((p) => p.props.defaultSize ?? 100 / panels.length);\n    const sum = raw.reduce((a, b) => a + b, 0);\n    return raw.map((r) => (r / sum) * 100);\n  })();\n  const [sizes, setSizes] = useState<number[]>(() => {\n    if (storageKey && typeof window !== \"undefined\")\n      try {\n        const v = JSON.parse(localStorage.getItem(storageKey) ?? \"null\");\n        if (Array.isArray(v) && v.length === panels.length) return v;\n      } catch {\n        /* ignore */\n      }\n    return defaults;\n  });\n  const [total, setTotal] = useState(0);\n  const [dragging, setDragging] = useState(false);\n  // panes the person unfolded while the room was too small for them: the room does not fold them again\n  const [kept, setKept] = useState<ReadonlySet<string>>(() => new Set());\n  const box = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(box, ref);\n  const restore = useRef<number[]>([]); // sizes before a fold, per pane\n  const drag = useRef<{ i: number; start: number; from: number[] } | null>(null);\n  const horizontal = direction === \"horizontal\";\n\n  useLayoutEffect(() => {\n    const el = box.current;\n    if (!el) return;\n    const measure = () => setTotal(horizontal ? el.clientWidth : el.clientHeight);\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [horizontal]);\n  // once the room holds every open pane again, the room's folds (and the panes kept from them) are forgotten\n  const roomy = total > 0 && panels.reduce((n, p, i) => n + (sizes[i] > 0 ? (p.props.minSize ?? 80) : 0), 0) <= total;\n  useEffect(() => {\n    if (roomy) setKept((k) => (k.size ? new Set() : k));\n  }, [roomy]);\n  const commit = (next: number[]) => {\n    setSizes(next);\n    onLayoutChange?.(next);\n    if (storageKey)\n      try {\n        localStorage.setItem(storageKey, JSON.stringify(next));\n      } catch {\n        /* storage blocked */\n      }\n  };\n  useEffect(() => {\n    if (sizes.length !== panels.length) setSizes(defaults);\n  }, [panels.length]); // eslint-disable-line react-hooks/exhaustive-deps -- panes added or removed\n\n  const pct = (px: number) => (total ? (px / total) * 100 : 0);\n  const minPx = panels.map((p) => p.props.minSize ?? 80);\n  // Fold by rank: when the room cannot hold every open pane at its minimum, collapsible panes fold away (first ones\n  // first) until it can; they come back as the room grows. Not stored: the sizes the person chose stay as they were.\n  const folded = new Set<number>();\n  if (total) {\n    const need = () => panels.reduce((n, _, i) => n + (sizes[i] > 0 && !folded.has(i) ? minPx[i] : 0), 0);\n    for (let i = 0; i < panels.length && need() > total; i++)\n      if (panels[i].props.collapsible && sizes[i] > 0 && !kept.has(panels[i].props.id)) folded.add(i);\n  }\n  // what is shown: a folded pane's share goes to its nearest open neighbour\n  const shown = sizes.map((v, i) => (folded.has(i) ? 0 : v));\n  for (const i of folded) {\n    const to =\n      [...shown.keys()].slice(i + 1).find((j) => shown[j] > 0) ?? [...shown.keys()].reverse().find((j) => shown[j] > 0);\n    if (to !== undefined) shown[to] += sizes[i];\n  }\n  // a room still too small for the open panes' minimums shares it out in proportion, instead of spilling\n  const openMin = panels.reduce((n, _, i) => n + (shown[i] > 0 ? minPx[i] : 0), 0);\n  const squeeze = total && openMin > total ? total / openMin : 1;\n  const minOf = (i: number) => pct(minPx[i] * squeeze);\n  const maxOf = (i: number) => (panels[i]?.props.maxSize ? pct(panels[i].props.maxSize!) : 100);\n  /** Moves the divider after pane i by `delta` percent, within both neighbours' limits; folds or unfolds collapsibles. */\n  const shift = (from: number[], i: number, delta: number) => {\n    const next = [...from];\n    const pair = from[i] + from[i + 1];\n    let a = from[i] + delta;\n    const foldA = panels[i].props.collapsible,\n      foldB = panels[i + 1].props.collapsible;\n    if (foldA && a < minOf(i) / 2) a = 0;\n    else if (foldB && pair - a < minOf(i + 1) / 2) a = pair;\n    else a = Math.min(Math.max(a, minOf(i)), maxOf(i), pair - minOf(i + 1));\n    a = Math.max(0, Math.min(pair, a));\n    next[i] = a;\n    next[i + 1] = pair - a;\n    return next;\n  };\n\n  // One shared drag (BUILDING §7) for every divider of this group: a press on one of its own dividers (never a nested\n  // group's) starts from what is shown (a pane the room folded stays folded in what the drag commits); a release\n  // commits; Escape puts the sizes back uncommitted; a finger drags along the divider's axis, the page keeps the other.\n  const dividerAt = (target: EventTarget | null) => {\n    const d = (target as Element | null)?.closest?.<HTMLElement>(\"[data-slot=resizable-divider]\");\n    return d && d.closest(\"[data-slot=resizable-panels]\") === box.current ? Number(d.dataset.index) : -1;\n  };\n  useDrag(\n    box,\n    {\n      onPress: (p, e) => {\n        const i = dividerAt(e.target);\n        if (i < 0 || e.button !== 0) return false;\n        drag.current = { i, start: horizontal ? p.x : p.y, from: shown };\n        setDragging(true);\n      },\n      onMove: (p) => {\n        const d = drag.current;\n        if (!d) return;\n        const rtl = horizontal && getComputedStyle(box.current!).direction === \"rtl\";\n        const delta = pct((horizontal ? p.x : p.y) - d.start) * (rtl ? -1 : 1);\n        setSizes(shift(d.from, d.i, delta));\n      },\n      onEnd: () => {\n        if (!drag.current) return;\n        drag.current = null;\n        setDragging(false);\n        commit(sizes);\n      },\n      onTap: () => {\n        drag.current = null;\n        setDragging(false);\n      },\n      onAbandon: () => {\n        drag.current = null;\n        setDragging(false);\n      },\n      onCancel: () => {\n        const d = drag.current;\n        if (!d) return;\n        drag.current = null;\n        setDragging(false);\n        setSizes(d.from);\n      },\n    },\n    { axis: horizontal ? \"x\" : \"y\" },\n  );\n  const fold = (i: number) => {\n    const target = panels[i].props.collapsible ? i : panels[i + 1].props.collapsible ? i + 1 : -1;\n    if (target < 0) return;\n    const other = target === i ? i + 1 : i;\n    // folded by the room: unfolding it keeps it open (the other panes give way) until the room is big enough again\n    if (folded.has(target)) {\n      setKept((k) => new Set([...k, panels[target].props.id]));\n      return;\n    }\n    const next = [...sizes];\n    if (sizes[target] > 0) {\n      restore.current[target] = sizes[target];\n      next[other] += sizes[target];\n      next[target] = 0;\n    } else {\n      const back = restore.current[target] ?? Math.max(minOf(target), 20);\n      next[target] = back;\n      next[other] -= back;\n    }\n    commit(next);\n  };\n  const onKey = (i: number) => (e: KeyboardEvent<HTMLDivElement>) => {\n    const back = horizontal\n      ? getComputedStyle(e.currentTarget).direction === \"rtl\"\n        ? \"ArrowRight\"\n        : \"ArrowLeft\"\n      : \"ArrowUp\";\n    const fwd = horizontal ? (back === \"ArrowLeft\" ? \"ArrowRight\" : \"ArrowLeft\") : \"ArrowDown\";\n    const big = e.shiftKey ? STEP * 2 : STEP;\n    const map: Record<string, () => number[] | null> = {\n      [back]: () => shift(shown, i, -big),\n      [fwd]: () => shift(shown, i, big),\n      Home: () => shift(shown, i, -100),\n      End: () => shift(shown, i, 100),\n      Enter: () => {\n        fold(i);\n        return null;\n      },\n    };\n    const run = map[e.key];\n    if (!run) return;\n    e.preventDefault();\n    const next = run();\n    if (next) commit(next);\n  };\n\n  return (\n    <div\n      ref={attach}\n      style={style}\n      role=\"group\"\n      aria-label={label}\n      data-slot=\"resizable-panels\"\n      data-direction={direction}\n      data-dragging={dragging ? \"\" : undefined}\n      className={cn(\n        \"flex h-full w-full overflow-hidden\",\n        horizontal ? \"flex-row\" : \"flex-col\",\n        classNames?.root,\n        className,\n      )}\n    >\n      {panels.map((panel, i) => (\n        <Fragment key={panel.props.id}>\n          <div\n            ref={panel.props.ref}\n            id={`pane-${panel.props.id}`}\n            data-slot=\"resizable-panel\"\n            role=\"region\"\n            aria-label={panel.props.label}\n            tabIndex={0}\n            data-collapsed={shown[i] === 0 ? \"\" : undefined}\n            data-folded-by-room={folded.has(i) ? \"\" : undefined}\n            inert={shown[i] === 0}\n            className={cn(styles.panel, \"min-h-0 min-w-0 overflow-auto\", classNames?.panel, panel.props.className)}\n            style={{\n              ...panel.props.style,\n              flex: `${shown[i] ?? 0} 1 0px`,\n              [horizontal ? \"minWidth\" : \"minHeight\"]: shown[i] > 0 && total ? minPx[i] * squeeze : undefined,\n            }}\n          >\n            {panel}\n          </div>\n          {i < panels.length - 1 && (\n            <div\n              role=\"separator\"\n              tabIndex={0}\n              aria-label={i18n(\"resize\", { label: panel.props.label })}\n              aria-controls={`pane-${panel.props.id}`}\n              aria-orientation={horizontal ? \"vertical\" : \"horizontal\"}\n              aria-valuenow={Math.round(shown[i] ?? 0)}\n              aria-valuemin={0}\n              aria-valuemax={100}\n              data-index={i}\n              onKeyDown={onKey(i)}\n              onDoubleClick={() => {\n                setKept(new Set());\n                commit(defaults);\n              }}\n              data-slot=\"resizable-divider\"\n              data-folded={shown[i] === 0 || shown[i + 1] === 0 ? \"\" : undefined}\n              className={cn(styles.divider, classNames?.divider)}\n            >\n              <span data-slot=\"resizable-grip\" aria-hidden className={cn(styles.grip, classNames?.grip)} />\n            </div>\n          )}\n        </Fragment>\n      ))}\n    </div>\n  );\n}\n\nexport default ResizablePanels;\n"},{"path":"beamline/hooks/use-composed-ref.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-composed-ref.ts","content":"\"use client\";\n\nimport { useCallback, type Ref, type RefObject } from \"react\";\n\n/** Keep a control's measurement/focus ref and the caller's ref, including React 19 callback cleanup. */\nexport function useComposedRef<T>(internal: RefObject<T | null>, external?: Ref<T>) {\n  return useCallback(\n    (node: T | null) => {\n      internal.current = node;\n      if (typeof external === \"function\") {\n        const cleanup = external(node);\n        if (typeof cleanup === \"function\")\n          return () => {\n            internal.current = null;\n            cleanup();\n          };\n      } else if (external) external.current = node;\n    },\n    [internal, external],\n  );\n}\n"},{"path":"beamline/hooks/use-drag.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-drag.ts","content":"\"use client\";\n\nimport { useEffect, useRef, useState, type RefObject } from \"react\";\nimport { VelocityTracker } from \"../lib/gesture\";\n\n/** Where the pointer is during a drag, relative to where it was pressed, with its velocity (px/s) over the last 100 ms. */\nexport interface DragPoint {\n  /** Pointer position in client coordinates. */\n  x: number;\n  y: number;\n  /** Distance from the press. */\n  dx: number;\n  dy: number;\n  /** Velocity in px/s (read from the last 100 ms, so a release carries the flick, not the whole drag). */\n  vx: number;\n  vy: number;\n  pointerType: string;\n  /** Shift was held (a part may offer a second mode on it: move one task alone, a finer step). */\n  shift: boolean;\n}\n\nexport interface DragHandlers {\n  /** A press landed on the element. Return false to leave it alone (a press on a nested button or link). */\n  onPress?: (point: DragPoint, event: PointerEvent) => boolean | void;\n  /** The pointer moved past the threshold: the drag begins (the press point is in `point.x - point.dx`). */\n  onStart?: (point: DragPoint) => void;\n  /** The pointer moved during a drag. `event` is the browser's own, for what a point does not carry: a pen's pressure,\n   *  the coalesced samples a fast stroke packs into one event (a drawing surface). */\n  onMove?: (point: DragPoint, event: PointerEvent) => void;\n  /** Released after a drag, with the release velocity. */\n  onEnd?: (point: DragPoint) => void;\n  /** Released without moving past the threshold: a tap or click (the part's tap route, WCAG 2.5.7). */\n  onTap?: (point: DragPoint) => void;\n  /** Escape, a lost pointer or the browser taking the gesture (a page scroll): put things back as they were. */\n  onCancel?: () => void;\n  /** A press that ended as neither a drag nor a tap: a finger that went on to scroll the page, or a pointer the browser\n   *  took before the threshold. Undo whatever `onPress` prepared. */\n  onAbandon?: () => void;\n}\n\nexport interface DragOptions {\n  /** The axis the part moves on. On touch, a drag that starts mostly across it is left to the page (a scroll). */\n  axis?: \"x\" | \"y\" | \"both\";\n  /** Pixels before a press becomes a drag: 3 for a mouse, 8 for a finger or pen unless given. */\n  threshold?: number;\n  disabled?: boolean;\n}\n\n/**\n * The pointer side of a hand on a part, shared by everything dragged, thrown or pulled (a thumb, a puck, a stub, a\n * task, a cable). It listens natively on the element, so moves never render React: the part writes positions in its\n * handlers (a spring, a transform, a custom property) and only `dragging`, which changes at the start and end, is\n * state (for `data-state`).\n *\n * A press becomes a drag past a small threshold, then the pointer is captured so the drag continues outside the\n * element; a release without moving is a tap (`onTap`), the route every drag needs for people who cannot drag. Escape,\n * `pointercancel` and a lost capture call `onCancel`, and the part restores what it showed before the press. On touch the\n * element needs a `touch-action` that leaves the other axis to the page (`pan-y` for a part dragged sideways; never\n * `none` on more than the dragged surface, which would block zoom), and a drag that starts across its axis is dropped\n * so the page can scroll.\n */\nexport function useDrag<T extends HTMLElement | SVGElement>(\n  ref: RefObject<T | null>,\n  handlers: DragHandlers,\n  { axis = \"both\", threshold, disabled = false }: DragOptions = {},\n): { dragging: boolean } {\n  const [dragging, setDragging] = useState(false);\n  const latest = useRef(handlers);\n  latest.current = handlers;\n\n  useEffect(() => {\n    // an SVG part (a chart's brush) is dragged the same way; its pointer events are typed as an HTML element's\n    const el = ref.current as HTMLElement | null;\n    if (!el || disabled) return;\n    const doc = el.ownerDocument;\n    // Escape during a drag belongs to the drag: listened on the window's capture phase, it runs before any dialog or\n    // popover listening on the document, which would otherwise also close around the cancelled drag\n    const win = doc.defaultView ?? window;\n    let active: {\n      id: number;\n      x0: number;\n      y0: number;\n      type: string;\n      started: boolean;\n      tracker: VelocityTracker;\n    } | null = null;\n\n    const point = (e: PointerEvent): DragPoint => {\n      const a = active!;\n      const [vx, vy] = a.tracker.velocity(e.timeStamp);\n      return {\n        x: e.clientX,\n        y: e.clientY,\n        dx: e.clientX - a.x0,\n        dy: e.clientY - a.y0,\n        vx,\n        vy,\n        pointerType: e.pointerType,\n        shift: e.shiftKey,\n      };\n    };\n\n    const finish = () => {\n      if (active?.started && el.hasPointerCapture?.(active.id)) el.releasePointerCapture(active.id);\n      active = null;\n      doc.removeEventListener(\"pointermove\", move);\n      doc.removeEventListener(\"pointerup\", up);\n      doc.removeEventListener(\"pointercancel\", cancel);\n      win.removeEventListener(\"keydown\", key, true);\n      doc.removeEventListener(\"selectstart\", hold, true);\n      doc.removeEventListener(\"dragstart\", hold, true);\n      el.removeEventListener(\"lostpointercapture\", lost);\n      setDragging(false);\n    };\n\n    const cancel = () => {\n      if (!active) return;\n      const started = active.started;\n      finish();\n      if (started) latest.current.onCancel?.();\n      else latest.current.onAbandon?.();\n    };\n\n    // only the part's own capture ending cancels: a finger's press is captured by the child it landed on, and moving\n    // the capture to the part fires lostpointercapture on that child, which bubbles here\n    const lost = (e: PointerEvent) => {\n      if (e.target === el && active?.started && e.pointerId === active.id) cancel();\n    };\n\n    const key = (e: KeyboardEvent) => {\n      if (e.key !== \"Escape\" || !active?.started) return;\n      e.preventDefault();\n      e.stopImmediatePropagation();\n      cancel();\n    };\n\n    // while the hand holds the part, the page neither selects text nor starts its own drag and drop (a selection left\n    // by an earlier drag would otherwise be dragged, and the browser cancels the pointer)\n    const hold = (e: Event) => e.preventDefault();\n\n    const move = (e: PointerEvent) => {\n      if (!active || e.pointerId !== active.id) return;\n      active.tracker.add(e.timeStamp, e.clientX, e.clientY);\n      const p = point(e);\n      if (!active.started) {\n        const limit = threshold ?? (active.type === \"mouse\" ? 3 : 8);\n        if (Math.hypot(p.dx, p.dy) < limit) return;\n        // a finger that starts across the part's axis is scrolling the page\n        const across = axis === \"x\" ? Math.abs(p.dy) > Math.abs(p.dx) : axis === \"y\" && Math.abs(p.dx) > Math.abs(p.dy);\n        if (active.type !== \"mouse\" && across) {\n          finish();\n          latest.current.onAbandon?.();\n          return;\n        }\n        active.started = true;\n        doc.getSelection()?.removeAllRanges();\n        el.setPointerCapture?.(active.id);\n        el.addEventListener(\"lostpointercapture\", lost);\n        setDragging(true);\n        latest.current.onStart?.(p);\n      }\n      latest.current.onMove?.(p, e);\n    };\n\n    const up = (e: PointerEvent) => {\n      if (!active || e.pointerId !== active.id) return;\n      active.tracker.add(e.timeStamp, e.clientX, e.clientY);\n      const p = point(e);\n      const started = active.started;\n      finish();\n      if (started) latest.current.onEnd?.(p);\n      else latest.current.onTap?.(p);\n    };\n\n    const down = (e: PointerEvent) => {\n      if (active || !e.isPrimary || (e.pointerType === \"mouse\" && e.button !== 0)) return;\n      const tracker = new VelocityTracker();\n      tracker.add(e.timeStamp, e.clientX, e.clientY);\n      active = { id: e.pointerId, x0: e.clientX, y0: e.clientY, type: e.pointerType, started: false, tracker };\n      if (latest.current.onPress?.(point(e), e) === false) {\n        active = null;\n        return;\n      }\n      doc.addEventListener(\"pointermove\", move);\n      doc.addEventListener(\"pointerup\", up);\n      doc.addEventListener(\"pointercancel\", cancel);\n      win.addEventListener(\"keydown\", key, true);\n      doc.addEventListener(\"selectstart\", hold, true);\n      doc.addEventListener(\"dragstart\", hold, true);\n    };\n\n    el.addEventListener(\"pointerdown\", down);\n    return () => {\n      el.removeEventListener(\"pointerdown\", down);\n      if (active) finish();\n    };\n  }, [ref, axis, threshold, disabled]);\n\n  return { dragging };\n}\n"},{"path":"beamline/lib/gesture.ts","type":"registry:file","target":"src/ui/beamline/lib/gesture.ts","content":"import { readMotionFactor } from \"./motion-tokens\";\n\n/**\n * The arithmetic of a hand on a part, shared by every part that is dragged, thrown or pulled (useDrag is the pointer\n * side). Three pieces, each from how real interfaces feel (Apple, \"Designing Fluid Interfaces\", WWDC 2018):\n *\n * - velocity: read from the last ~100 ms of movement, so the release carries the speed of the flick, not of the whole drag;\n * - projection: where a thrown thing would come to rest if it slid on with the look's carry (`project`), so a part\n *   can decide on release (the nearest value, step or preset to that point) instead of after an animation;\n * - give: past a limit the part follows the hand less and less (`rubberband`) instead of stopping dead, and springs back.\n *\n * How much a part carries and gives is the look's material, read once per gesture from the element (`readHand`):\n * `--m-hand-carry`, `--m-hand-give` and `--m-hand-bounce` (preset-source.css), with `--m-morph-weight` scaling how\n * long things take to settle (times a motion token's duration, which carries the speed setting). Reduced motion\n * carries nothing, gives nothing and never bounces.\n */\n\nexport interface Hand {\n  /** How far a release carries: 0 stops where the hand let go, 1 slides on freely (see `project`). */\n  carry: number;\n  /** How much a part follows the hand past a limit: 0 is a hard stop, 0.55 a phone's rubber band. */\n  give: number;\n  /** Bounce of a released held object (0 critically damped). Only for what the pointer held. */\n  bounce: number;\n  /** The look's weight (`--m-morph-weight`): multiply a motion token's duration by it. The token already carries the\n   *  speed setting, so the weight never does. */\n  weight: number;\n  /** Reduced or no motion: land at once. */\n  reduced: boolean;\n}\n\n/** The hand the defaults describe; also what a server or a test without styles gets. */\nexport const DEFAULT_HAND: Hand = { carry: 0.8, give: 0.45, bounce: 0.14, weight: 1, reduced: false };\n\nconst num = (style: CSSStyleDeclaration, name: string, fallback: number) => {\n  const v = Number.parseFloat(style.getPropertyValue(name));\n  return Number.isFinite(v) ? v : fallback;\n};\n\n/** The look's physics at `el`, read once at the start of a gesture (never per frame). */\nexport function readHand(el: Element | null | undefined): Hand {\n  if (!el || typeof getComputedStyle === \"undefined\") return DEFAULT_HAND;\n  const style = getComputedStyle(el);\n  const factor = readMotionFactor(el);\n  // the motion factor is ~0.005 under reduced motion and 1 at the foundation default\n  const reduced = factor < 0.05;\n  if (reduced) return { carry: 0, give: 0, bounce: 0, weight: 1, reduced: true };\n  return {\n    carry: num(style, \"--m-hand-carry\", DEFAULT_HAND.carry),\n    give: num(style, \"--m-hand-give\", DEFAULT_HAND.give),\n    bounce: num(style, \"--m-hand-bounce\", DEFAULT_HAND.bounce),\n    weight: num(style, \"--m-morph-weight\", 1),\n    reduced: false,\n  };\n}\n\n/** Samples of a pointer's path, kept for the last `window` ms, to read its velocity at release. */\nexport class VelocityTracker {\n  private samples: { t: number; x: number; y: number }[] = [];\n  // a declared field, not a parameter property: shipped source must also compile under erasableSyntaxOnly (Vite's\n  // TypeScript template turns it on), where TypeScript-only syntax with runtime meaning is refused\n  private readonly window: number;\n  constructor(window = 100) {\n    this.window = window;\n  }\n\n  add(t: number, x: number, y: number) {\n    this.samples.push({ t, x, y });\n    const from = t - this.window;\n    while (this.samples.length > 2 && this.samples[0].t < from) this.samples.shift();\n  }\n\n  /** Units per second over the kept window; 0 when the hand stopped before letting go (a pause of more than the window). */\n  velocity(now = this.samples.at(-1)?.t ?? 0): [number, number] {\n    const s = this.samples.filter((p) => p.t >= now - this.window);\n    if (s.length < 2) return [0, 0];\n    // least squares slope over the window: one jittery last sample does not decide the throw\n    const n = s.length;\n    const mt = s.reduce((a, p) => a + p.t, 0) / n;\n    const mx = s.reduce((a, p) => a + p.x, 0) / n;\n    const my = s.reduce((a, p) => a + p.y, 0) / n;\n    let tt = 0;\n    let tx = 0;\n    let ty = 0;\n    for (const p of s) {\n      tt += (p.t - mt) ** 2;\n      tx += (p.t - mt) * (p.x - mx);\n      ty += (p.t - mt) * (p.y - my);\n    }\n    if (tt === 0) return [0, 0];\n    return [(tx / tt) * 1000, (ty / tt) * 1000];\n  }\n\n  reset() {\n    this.samples = [];\n  }\n}\n\n/**\n * How far something released at `velocity` (units/s) slides before friction stops it, with the look's carry. `rate` is\n * the speed kept per millisecond: 0.99 (the default) suits controls on a short scale (a thumb, a puck, a cut), where a\n * flick should carry a little way; 0.998 is a phone list's deceleration, for long scrolling content.\n */\nexport function project(velocity: number, carry: number, rate = 0.99): number {\n  if (carry <= 0) return 0;\n  return (((velocity / 1000) * rate) / (1 - rate)) * carry;\n}\n\n/**\n * Where a part drawn `overshoot` units past its limit sits, given the room `extent` it moves in: it follows less and\n * less the further it goes and never reaches `extent`. `give` 0 is a hard stop.\n */\nexport function rubberband(overshoot: number, extent: number, give: number): number {\n  if (give <= 0 || extent <= 0) return 0;\n  const sign = Math.sign(overshoot);\n  const x = Math.abs(overshoot);\n  return sign * (1 - 1 / ((x * give) / extent + 1)) * extent;\n}\n\n/** `value` held inside [min, max], with the part's give past either end. */\nexport function within(value: number, min: number, max: number, extent: number, give: number): number {\n  if (value < min) return min + rubberband(value - min, extent, give);\n  if (value > max) return max + rubberband(value - max, extent, give);\n  return value;\n}\n\n/** The nearest of `stops` to `value` (a projected landing point picks its step or preset this way). */\nexport function nearest(value: number, stops: readonly number[]): number {\n  let best = stops[0];\n  for (const s of stops) if (Math.abs(s - value) < Math.abs(best - value)) best = s;\n  return best;\n}\n"},{"path":"beamline/lib/utils.ts","type":"registry:file","target":"src/ui/beamline/lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n/* The type roles (utilities.css) and the 2xs step are sizes, not text colours: without this, tailwind-merge reads\n   `text-overline` as a colour and drops it next to `text-ink-3`. A role and a size step still replace each other.\n   Likewise `font-title` is the look's title weight, not a family: read as a family it dropped `font-display` beside it,\n   and every merged page title lost the look's display face. */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-weight\": [{ font: [\"title\"] }],\n      \"font-family\": [{ font: [\"display\", \"numeric\", \"figure\"] }],\n      \"font-size\": [\n        {\n          text: [\n            \"2xs\",\n            \"display\",\n            \"title\",\n            \"heading\",\n            \"subheading\",\n            \"body\",\n            \"body-sm\",\n            \"reading\",\n            \"label\",\n            \"caption\",\n            \"overline\",\n            \"numeric\",\n            \"code\",\n          ],\n        },\n      ],\n    },\n  },\n});\n\n/** Merge class names, letting later Tailwind utilities win over earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/** A small seeded random generator (LCG): the same seed gives the same sequence, for stable sample data and jitter. */\nexport function rng(seed = 1) {\n  let s = seed >>> 0 || 1;\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n"}],"docs":"Resizable panels is in src/ui/beamline/components/resizable-panels/resizable-panels.tsx. The source is in src/ui/beamline (ui/beamline in a project without src/). After Tailwind, import its styles/beamline.css once, then styles/look-prism.css in your app stylesheet (use relative paths). Keep the shared base before all chosen looks. Wrap your app in <UIRoot preset={prism}> with UIRoot from its runtime/appearance and prism from its looks/prism.  shadcn replaces files when asked; use the Beamline managed-source CLI from the start for three-way upgrades that preserve edits.","meta":{"free":true,"version":"0.2.0-rc.42","library":"https://beamline.io/components/resizable-panels"}}