{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"table-of-contents","type":"registry:component","title":"Table of contents","description":"The sections of a long page beside it, with the one being read marked as you scroll: the marker travels down the rail between sections, the rail fills with how far you have read, a click glides to the section and hands it focus; the quiet variant is a column of ticks that opens into names when pointed at, and a narrow room folds it into one “On this page” line.","categories":["navigation"],"dependencies":["@radix-ui/react-direction@1.1.4","clsx@^2.1.1","lucide-react@^1.48.0","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/table-of-contents/messages.ts","type":"registry:file","target":"src/ui/beamline/components/table-of-contents/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  title: \"On this page\",\n  onThisPage: \"On this page\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/table-of-contents/table-of-contents.module.css","type":"registry:file","target":"src/ui/beamline/components/table-of-contents/table-of-contents.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* The outline answers to its room: under 12rem it folds into one line naming the current section. */\n  .root {\n    --_rail: var(--pui-space-3);\n    position: relative;\n    display: grid;\n    min-width: 0;\n    align-content: start;\n    gap: var(--pui-space-2);\n    container-type: inline-size;\n  }\n  .title {\n    padding-inline-start: var(--_rail);\n    color: var(--pui-ink-3);\n    font-family: var(--pui-font-label);\n    font-size: var(--pui-text-2xs);\n    letter-spacing: var(--pui-tracking-eyebrow);\n    text-transform: uppercase;\n  }\n  .list {\n    position: relative;\n    display: grid;\n    gap: 1px;\n  }\n  /* the rail: a hairline, the read part of it filled from the top as the page scrolls */\n  .rail {\n    position: absolute;\n    inset-block: 0;\n    inset-inline-start: 0;\n    inline-size: 1px;\n    background: var(--pui-line);\n  }\n  .read {\n    position: absolute;\n    inset-inline: 0;\n    top: 0;\n    block-size: 100%;\n    background: var(--pui-ink-3);\n    transform: scaleY(var(--_read, 0));\n    transform-origin: top;\n  }\n  /* the marker sits over the rail at the current section and travels there, leading edge first */\n  .marker {\n    position: absolute;\n    pointer-events: none;\n  }\n  .marker::before {\n    content: \"\";\n    position: absolute;\n    inset-block: 3px;\n    inset-inline-start: 0;\n    inline-size: 2px;\n    border-radius: 1px;\n    background: var(--pui-accent-ink);\n  }\n  .item {\n    min-width: 0;\n    list-style: none;\n  }\n  .link {\n    display: flex;\n    min-height: var(--pui-size-7);\n    align-items: center;\n    gap: var(--pui-space-2);\n    padding-block: var(--pui-space-1);\n    padding-inline: var(--_rail) var(--pui-space-1);\n    border-radius: var(--pui-radius-chip);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-sm);\n    line-height: var(--pui-leading-tight);\n    text-decoration: none;\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .item[data-level=\"2\"] .link {\n    padding-inline-start: calc(var(--_rail) + var(--pui-space-3));\n  }\n  .item[data-level=\"3\"] .link {\n    padding-inline-start: calc(var(--_rail) + var(--pui-space-6));\n  }\n  .link:hover {\n    color: var(--pui-ink);\n    transition-duration: 0ms;\n  }\n  .link:focus-visible {\n    outline: var(--pui-focus-width) var(--pui-focus-style) var(--pui-focus-ring);\n    outline-offset: 0;\n  }\n  .item[data-current] .link {\n    color: var(--pui-ink);\n    font-weight: var(--pui-weight-medium);\n  }\n  .label {\n    min-width: 0;\n    text-wrap: pretty;\n  }\n\n  /* ticks: a column of short marks (longer for a section, shorter for its parts) that opens into names when pointed at\n     or focused; the current mark is the accent's */\n  .root[data-variant=\"ticks\"] .marker {\n    display: none;\n  }\n  .root[data-variant=\"ticks\"] .link {\n    padding-inline-start: 0;\n  }\n  .tick {\n    flex: none;\n    inline-size: var(--pui-space-4);\n    block-size: 2px;\n    border-radius: 1px;\n    background: var(--pui-line-strong);\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      inline-size var(--pui-duration-standard) var(--pui-ease-standard);\n  }\n  .item[data-level=\"2\"] .tick {\n    inline-size: var(--pui-space-2-5);\n    margin-inline-start: var(--pui-space-1-5);\n  }\n  .item[data-level=\"3\"] .tick {\n    inline-size: var(--pui-space-1-5);\n    margin-inline-start: var(--pui-space-2-5);\n  }\n  .item[data-current] .tick {\n    background: var(--pui-current-edge, var(--pui-accent-ink));\n  }\n  .link:hover .tick {\n    background: var(--pui-ink);\n  }\n  .root[data-variant=\"ticks\"] .label {\n    opacity: 0;\n    white-space: nowrap;\n    translate: calc(-4px * var(--pui-motion-travel, 1)) 0;\n    transition:\n      opacity var(--pui-motion-out) var(--pui-ease-default),\n      translate var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root[data-variant=\"ticks\"] .title {\n    padding-inline-start: 0;\n    opacity: 0;\n    transition: opacity var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root[data-variant=\"ticks\"]:is(:hover, :focus-within) :is(.label, .title) {\n    opacity: 1;\n    translate: 0 0;\n    transition-duration: var(--pui-duration-standard);\n  }\n\n  /* folded: one line naming the current section, the list opening in place under it */\n  .toggle {\n    display: flex;\n    inline-size: 100%;\n    min-height: var(--pui-size-10);\n    align-items: center;\n    gap: var(--pui-space-2);\n    padding-inline: var(--pui-space-3) var(--pui-space-2);\n    border-radius: var(--pui-radius-control);\n    cursor: pointer;\n    text-align: start;\n    transition: var(--m-transition);\n  }\n  .toggle:focus-visible {\n    outline: var(--pui-focus-width) var(--pui-focus-style) var(--pui-focus-ring);\n    outline-offset: 2px;\n  }\n  .toggleText {\n    display: grid;\n    min-width: 0;\n    flex: 1;\n  }\n  .toggleTitle {\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-2xs);\n  }\n  .toggleCurrent {\n    overflow: hidden;\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .chevron {\n    flex: none;\n    color: var(--pui-ink-3);\n    transition: rotate var(--pui-duration-fast) var(--pui-ease-standard);\n  }\n  .root[data-open] .chevron {\n    rotate: 180deg;\n  }\n  .drop {\n    padding-block: var(--pui-space-1);\n    animation: drop var(--pui-duration-standard) var(--pui-ease-standard);\n  }\n  @keyframes drop {\n    from {\n      opacity: 0;\n      translate: 0 calc(-4px * var(--pui-motion-travel, 1));\n    }\n  }\n}\n"},{"path":"beamline/components/table-of-contents/table-of-contents.tsx","type":"registry:file","target":"src/ui/beamline/components/table-of-contents/table-of-contents.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport {\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type ReactNode,\n  type Ref,\n  type RefObject,\n} from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\nimport { glideProps, useIndicator } from \"../../hooks/use-indicator\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport styles from \"./table-of-contents.module.css\";\n\nexport interface TocItem {\n  /** Stable identifier used to match this record across updates. Required. */\n  id: string;\n  /** Text displayed for this item. Required. */\n  label: string;\n  /** 1 for a section, 2 for a sub-section, 3 below that. Default 1. */\n  level?: number;\n}\n\nexport type TableOfContentsClassNames = Partial<\n  Record<\"root\" | \"title\" | \"list\" | \"item\" | \"link\" | \"marker\" | \"rail\" | \"toggle\", string>\n>;\n\nexport interface TableOfContentsProps {\n  /** The sections in page order; read from the page when left out. */\n  items?: TocItem[];\n  /** The element that scrolls the content; the window by default. */\n  scrollRoot?: RefObject<HTMLElement | null>;\n  /** Headings to read when `items` is left out. Default \"h2[id], h3[id]\". */\n  selector?: string;\n  /** Controlled id of the active section; null clears the indication; omit to use defaultValue. */\n  value?: string | null;\n  /** Initial id of the active section when uncontrolled. Defaults to null. */\n  defaultValue?: string | null;\n  /** Reports id of the active section after an edit; no callback by default. */\n  onValueChange?: (id: string | null) => void;\n  /** Pixels a sticky header covers at the top. Default 0. */\n  offset?: number;\n  /** Labelled rail or compact tick marks; defaults to rail. */\n  appearance?: \"rail\" | \"ticks\";\n  /** Legacy appearance setting; omitted by default. @deprecated Use appearance instead. */\n  variant?: \"rail\" | \"ticks\";\n  /** Fill the rail with how far the page is read. Default true. */\n  progress?: boolean;\n  /** Default \"On this page\"; null for none. */\n  title?: ReactNode;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: TableOfContentsClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLElement>;\n}\n\n/**\n * The sections of a long page beside it, the one being read marked as the page scrolls. The marker travels down the\n * rail, the rail fills with how far the page is read, and a click glides to a section and hands it focus. The ticks\n * variant is a column of short marks that opens into names on attention; a narrow room folds it into one line.\n */\nexport function TableOfContents({\n  items: itemsProp,\n  scrollRoot,\n  selector = \"h2[id], h3[id]\",\n  value,\n  defaultValue = null,\n  onValueChange,\n  offset = 0,\n  appearance,\n  variant: variantProp,\n  progress = true,\n  title: titleProp,\n  className,\n  classNames,\n  style,\n  ref,\n}: TableOfContentsProps) {\n  const i18n = useMessages(\"table-of-contents\", englishMessages);\n\n  useDeprecatedProp(\"table-of-contents\", \"variant\", \"appearance\", variantProp !== undefined);\n  const variant = appearance ?? variantProp ?? \"rail\";\n  const title = titleProp === undefined ? i18n(\"title\") : titleProp;\n\n  const [read, setRead] = useState<TocItem[]>([]);\n  const items = itemsProp ?? read;\n  // choosing the section already current is not a change, so the hook holds the value and the report is guarded\n  const [current, setCurrent] = useControllable<string | null>(value, defaultValue);\n  const [open, setOpen] = useState(false);\n  const root = useRef<HTMLElement>(null);\n  const attach = useComposedRef(root, ref);\n  const fill = useRef<HTMLSpanElement>(null);\n  // a click scrolls the page itself; the spy waits until it lands so the marker does not visit every section between\n  const steering = useRef<string | null>(null);\n  const motion = useMotionTokens(root);\n  const folded = useWidthAs(root, (w) => w > 0 && w < 192, false);\n  const [listRef, box] = useIndicator<HTMLOListElement>(\"[data-slot=table-of-contents-item][data-current]\", current);\n\n  const choose = (id: string | null) => {\n    setCurrent(id);\n    if (id !== current) onValueChange?.(id);\n  };\n\n  // headings read from the page when no items are given\n  useEffect(() => {\n    if (itemsProp) return;\n    const host: ParentNode = scrollRoot?.current ?? document;\n    const found = [...host.querySelectorAll<HTMLElement>(selector)].map((h) => ({\n      id: h.id,\n      label: h.textContent?.trim() ?? h.id,\n      level: Number(h.tagName[1]) > 2 ? Number(h.tagName[1]) - 1 : 1,\n    }));\n    setRead(found);\n  }, [itemsProp, scrollRoot, selector]);\n\n  // the spy: on scroll frames only, the last section whose heading has passed the line under the sticky header\n  useEffect(() => {\n    const scroller: HTMLElement | Window = scrollRoot?.current ?? window;\n    let frame = 0;\n    const measure = () => {\n      frame = 0;\n      const top = scroller instanceof Window ? 0 : scroller.getBoundingClientRect().top;\n      const line = top + offset + 12;\n      let at: string | null = items[0]?.id ?? null;\n      for (const it of items) {\n        const el = document.getElementById(it.id);\n        if (el && el.getBoundingClientRect().top <= line) at = it.id;\n      }\n      const el = scroller instanceof Window ? document.documentElement : scroller;\n      const max = el.scrollHeight - el.clientHeight;\n      const done = max > 0 ? Math.min(1, (scroller instanceof Window ? window.scrollY : el.scrollTop) / max) : 1;\n      // the last section is current once the page cannot scroll further\n      if (done >= 0.995 && items.length) at = items[items.length - 1].id;\n      fill.current?.style.setProperty(\"--_read\", String(done));\n      if (steering.current) {\n        if (steering.current === at || done >= 0.995) steering.current = null;\n        else return;\n      }\n      choose(at);\n    };\n    const onScroll = () => {\n      if (!frame) frame = requestAnimationFrame(measure);\n    };\n    measure();\n    scroller.addEventListener(\"scroll\", onScroll, { passive: true });\n    return () => {\n      scroller.removeEventListener(\"scroll\", onScroll);\n      cancelAnimationFrame(frame);\n    };\n  }); // every render: the items, offset and the controlled value are read fresh; the listener is cheap to swap\n\n  const go = (id: string) => {\n    const el = document.getElementById(id);\n    if (!el) return;\n    const scroller = scrollRoot?.current;\n    const top = scroller\n      ? el.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop - offset\n      : el.getBoundingClientRect().top + window.scrollY - offset;\n    steering.current = id;\n    choose(id);\n    (scroller ?? window).scrollTo({ top: Math.max(0, top - 8), behavior: motion.reduced ? \"auto\" : \"smooth\" });\n    // the heading takes focus without a second jump, so the keyboard continues from the section\n    if (!el.hasAttribute(\"tabindex\")) el.setAttribute(\"tabindex\", \"-1\");\n    el.focus({ preventScroll: true });\n    setOpen(false);\n  };\n\n  useLayoutEffect(() => {\n    if (!folded) setOpen(false);\n  }, [folded]);\n\n  const currentLabel = items.find((i) => i.id === current)?.label;\n  const list = (\n    <ol ref={listRef} data-slot=\"table-of-contents-list\" className={cn(styles.list, classNames?.list)}>\n      {variant === \"rail\" && (\n        <span aria-hidden className={cn(styles.rail, classNames?.rail)} data-slot=\"table-of-contents-rail\">\n          {progress && <span ref={fill} className={styles.read} />}\n        </span>\n      )}\n      {box && (\n        <span\n          aria-hidden\n          data-slot=\"table-of-contents-marker\"\n          {...glideProps(box)}\n          className={cn(styles.marker, \"glide glide-2d\", classNames?.marker)}\n        />\n      )}\n      {items.map((it) => {\n        const on = it.id === current;\n        return (\n          <li\n            key={it.id}\n            data-slot=\"table-of-contents-item\"\n            data-current={on ? \"\" : undefined}\n            data-level={Math.min(3, it.level ?? 1)}\n            className={cn(styles.item, classNames?.item)}\n          >\n            <a\n              href={`#${it.id}`}\n              aria-current={on ? \"location\" : undefined}\n              className={cn(styles.link, classNames?.link)}\n              onClick={(e) => {\n                e.preventDefault();\n                go(it.id);\n              }}\n            >\n              {variant === \"ticks\" && <span aria-hidden className={styles.tick} />}\n              <span className={styles.label}>{it.label}</span>\n            </a>\n          </li>\n        );\n      })}\n    </ol>\n  );\n\n  return (\n    <nav\n      ref={attach}\n      aria-label={typeof title === \"string\" ? title : i18n(\"onThisPage\")}\n      data-slot=\"table-of-contents\"\n      data-variant={variant}\n      data-folded={folded ? \"\" : undefined}\n      data-open={open ? \"\" : undefined}\n      className={cn(styles.root, classNames?.root, className)}\n      style={style}\n    >\n      {folded ? (\n        <>\n          <button\n            type=\"button\"\n            aria-expanded={open}\n            className={cn(styles.toggle, \"ix m-control\", classNames?.toggle)}\n            onClick={() => setOpen((o) => !o)}\n            onKeyDown={(e) => {\n              if (e.key === \"Escape\") setOpen(false);\n            }}\n          >\n            <span className={styles.toggleText}>\n              <span className={styles.toggleTitle}>{title ?? i18n(\"onThisPage\")}</span>\n              {currentLabel && <span className={styles.toggleCurrent}>{currentLabel}</span>}\n            </span>\n            <ChevronDown aria-hidden className={cn(styles.chevron, \"size-4\")} />\n          </button>\n          {open && (\n            <div\n              className={styles.drop}\n              onKeyDown={(e) => {\n                if (e.key === \"Escape\") setOpen(false);\n              }}\n            >\n              {list}\n            </div>\n          )}\n        </>\n      ) : (\n        <>\n          {title !== null && (\n            <p className={cn(styles.title, classNames?.title)} data-slot=\"table-of-contents-title\">\n              {title}\n            </p>\n          )}\n          {list}\n        </>\n      )}\n    </nav>\n  );\n}\n\nexport default TableOfContents;\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-content-width.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-content-width.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\nconst same = (width: number) => width;\n\n/**\n * The width of an element's content box, kept current with a ResizeObserver. It is first read in a layout effect, so\n * a layout chosen by width (a narrow mode, a chart's scale) is right in the first frame instead of jumping a frame\n * later. `fallback` stands in on the server and until the element exists; `key` re-measures when the element is\n * replaced (one that exists only in one view).\n */\nexport function useContentWidth(ref: RefObject<HTMLElement | null>, fallback: number, key?: unknown): number {\n  return useWidthAs(ref, same, fallback, key);\n}\n\n/**\n * The same width, kept as what a part needs from it (`(w) => w < 640` for a narrow mode): the part renders again only\n * when that changes, not at every pixel.\n */\nexport function useWidthAs<T>(\n  ref: RefObject<HTMLElement | null>,\n  read: (width: number) => T,\n  fallback: T,\n  key?: unknown,\n): T {\n  const [value, setValue] = useState(fallback);\n  const latest = useRef(read);\n  latest.current = read;\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    // the computed width is fractional and ignores transforms, like the observer's content box\n    const s = getComputedStyle(el);\n    const edges = [\"paddingLeft\", \"paddingRight\", \"borderLeftWidth\", \"borderRightWidth\"] as const;\n    const box = parseFloat(s.width);\n    const outer = Number.isFinite(box) ? box : el.offsetWidth;\n    const inner =\n      Number.isFinite(box) && s.boxSizing !== \"border-box\" ? 0 : edges.reduce((w, k) => w + parseFloat(s[k]), 0);\n    setValue(latest.current(outer - inner));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(latest.current(e.contentRect.width)));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key]);\n  return value;\n}\n\n/**\n * The height of an element's content box, kept current like `useContentWidth`: for parts that lay out at whatever\n * height their parent gives them (a chart, a map or a book that fills its panel). Read in a layout effect, so the first\n * frame is right; with `enabled` false nothing is measured and the fallback stands.\n */\nexport function useContentHeight(\n  ref: RefObject<HTMLElement | null>,\n  fallback: number,\n  key?: unknown,\n  enabled = true,\n): number {\n  const [value, setValue] = useState(fallback);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!enabled || !el) return;\n    const s = getComputedStyle(el);\n    setValue(el.clientHeight - (parseFloat(s.paddingTop) || 0) - (parseFloat(s.paddingBottom) || 0));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(e.contentRect.height));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key, enabled]);\n  return enabled ? value : fallback;\n}\n"},{"path":"beamline/hooks/use-controllable.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-controllable.ts","content":"\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\n/**\n * One value in both forms (rules/CONVENTIONS.md): the prop while it is passed, otherwise the element's own copy that\n * starts at `initial` (a value, or a function that makes it once). The setter takes a value or an updater; an updater\n * reads the latest value even twice in one event, and every change reaches `onChange`, controlled or not.\n * `Next` is what a change can carry when it is narrower than the state: a range that starts empty (null) but is only\n * ever changed to a range.\n */\nexport function useControllable<T, Next extends T = T>(\n  value: T | undefined,\n  initial: T | (() => T),\n  onChange?: (next: Next) => void,\n) {\n  const [own, setOwn] = useState<T>(initial);\n  const current = value !== undefined ? value : own;\n  const latest = useRef(current);\n  latest.current = current;\n  const set = useCallback(\n    (next: Next | ((was: T) => Next)) => {\n      const resolved = typeof next === \"function\" ? (next as (was: T) => Next)(latest.current) : next;\n      latest.current = resolved;\n      if (value === undefined) setOwn(resolved);\n      onChange?.(resolved);\n    },\n    [value, onChange],\n  );\n  return [current, set] as const;\n}\n"},{"path":"beamline/hooks/use-deprecated-prop.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-deprecated-prop.ts","content":"\"use client\";\n\nimport { useEffect } from \"react\";\n\n// Bundlers replace NODE_ENV for browser builds; this declaration emits no Node dependency.\ndeclare const process: { env: { NODE_ENV?: string } };\n\nconst warned = new Set<string>();\n\n/** Report a legacy prop once after mount in development; production and server renders stay silent. */\nexport function useDeprecatedProp(component: string, prop: string, replacement: string, supplied: boolean) {\n  useEffect(() => {\n    if (process.env.NODE_ENV !== \"development\" || !supplied) return;\n    const key = `${component}.${prop}`;\n    if (warned.has(key)) return;\n    warned.add(key);\n    console.warn(`[Beamline] ${key} is deprecated. Use ${replacement} instead; the legacy prop still works.`);\n  }, [component, prop, replacement, supplied]);\n}\n"},{"path":"beamline/hooks/use-indicator.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-indicator.ts","content":"\"use client\";\n\nimport { APPEARANCE_EVENT } from \"../lib/motion-tokens\";\nimport { useLayoutEffect, useRef, useState, type CSSProperties, type RefObject } from \"react\";\n\nexport interface IndicatorBox {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  /** False on the first placement, so the indicator appears in place instead of sliding in from the corner. */\n  ready: boolean;\n  /** Which way the last move went on each axis (-1 left or up, 1 right or down, 0 still), for the edge that leads. */\n  dx: -1 | 0 | 1;\n  dy: -1 | 0 | 1;\n  /** choice: a selection changed; layout: the same choice moved because the page reflowed (a resize, a font, a\n   *  type scale). A surface may travel for a choice and should simply be there after a reflow. */\n  cause: \"choice\" | \"layout\";\n}\n\ntype Box = Pick<IndicatorBox, \"x\" | \"y\" | \"w\" | \"h\">;\nconst same = (a: IndicatorBox | null, b: Box) => a !== null && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;\n\n/**\n * Follows the chosen item inside a container: the thumb of a segmented control, the pill or line of tabs, the ring of\n * radio cards. Returns a ref for the container and the box of the first element matching `selector` in it, relative\n * to the container (null when nothing matches). It measures before paint whenever `key` changes, and again when the\n * container resizes or a selection attribute changes inside it (for parts whose choice lives in a library, like tabs).\n */\nexport function useIndicator<T extends HTMLElement>(\n  selector: string,\n  key?: unknown,\n  onPlace?: (item: HTMLElement, container: T) => void,\n  /** offset (default): the item's offset box, for items whose offset parent is the container. rect: measured on screen\n   *  relative to the container, for items nested in positioned cells (a calendar day in its table cell). */\n  measure: \"offset\" | \"rect\" = \"offset\",\n): [RefObject<T | null>, IndicatorBox | null] {\n  const ref = useRef<T>(null);\n  const [box, setBox] = useState<IndicatorBox | null>(null);\n  const placed = useRef(onPlace);\n  useLayoutEffect(() => {\n    placed.current = onPlace;\n  });\n  useLayoutEffect(() => {\n    const container = ref.current;\n    if (!container) return;\n    const place = (cause: IndicatorBox[\"cause\"] = \"choice\") => {\n      const item = container.querySelector<HTMLElement>(selector);\n      if (!item) {\n        setBox(null);\n        return;\n      }\n      let next = { x: item.offsetLeft, y: item.offsetTop, w: item.offsetWidth, h: item.offsetHeight };\n      if (measure === \"rect\") {\n        const a = item.getBoundingClientRect();\n        const c = container.getBoundingClientRect();\n        next = {\n          x: Math.round(a.left - c.left - container.clientLeft + container.scrollLeft),\n          y: Math.round(a.top - c.top - container.clientTop + container.scrollTop),\n          w: item.offsetWidth,\n          h: item.offsetHeight,\n        };\n      }\n      setBox((b) =>\n        same(b, next)\n          ? b\n          : {\n              ...next,\n              cause,\n              ready: b !== null,\n              dx: b ? (Math.sign(next.x - b.x) as IndicatorBox[\"dx\"]) : 0,\n              dy: b ? (Math.sign(next.y - b.y) as IndicatorBox[\"dy\"]) : 0,\n            },\n      );\n      placed.current?.(item, container);\n    };\n    const resizes = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(() => place(\"layout\"));\n    resizes?.observe(container);\n    const observed = new Set<Element>();\n    const observeItems = () => {\n      // A full-width fixed-height list can stay the same size while its tabs/radio choices grow with type or labels.\n      // Ignore the decorative indicator itself, whose own dimensions are the output of this measurement.\n      const items = new Set([...container.children].filter((child) => child.getAttribute(\"aria-hidden\") !== \"true\"));\n      for (const old of observed)\n        if (!items.has(old)) {\n          resizes?.unobserve(old);\n          observed.delete(old);\n        }\n      for (const item of items)\n        if (!observed.has(item)) {\n          resizes?.observe(item);\n          observed.add(item);\n        }\n    };\n    observeItems();\n    place();\n    const mutations = new MutationObserver(() => {\n      observeItems();\n      place(\"choice\");\n    });\n    mutations.observe(container, {\n      subtree: true,\n      childList: true,\n      attributes: true,\n      attributeFilter: [\"data-state\", \"aria-checked\", \"aria-selected\", \"aria-current\", \"data-active\", \"data-open\"],\n    });\n    const reflow = () => place(\"layout\");\n    container.ownerDocument.addEventListener(APPEARANCE_EVENT, reflow);\n    container.ownerDocument.fonts?.addEventListener(\"loadingdone\", reflow);\n    return () => {\n      mutations.disconnect();\n      resizes?.disconnect();\n      container.ownerDocument.removeEventListener(APPEARANCE_EVENT, reflow);\n      container.ownerDocument.fonts?.removeEventListener(\"loadingdone\", reflow);\n    };\n  }, [selector, key, measure]);\n  return [ref, box];\n}\n\n/** The box as custom properties, for a stylesheet to place the indicator with: --pui-indicator-x/-y/-w/-h, and its\n *  four edges --pui-glide-l/-r/-t/-b, which the `glide` class moves one at a time. */\nexport function indicatorStyle(box: IndicatorBox): CSSProperties {\n  return {\n    \"--pui-indicator-x\": `${box.x}px`,\n    \"--pui-indicator-y\": `${box.y}px`,\n    \"--pui-indicator-w\": `${box.w}px`,\n    \"--pui-indicator-h\": `${box.h}px`,\n    \"--pui-glide-l\": `${box.x}px`,\n    \"--pui-glide-r\": `${box.x + box.w}px`,\n    \"--pui-glide-t\": `${box.y}px`,\n    \"--pui-glide-b\": `${box.y + box.h}px`,\n  } as CSSProperties;\n}\n\nconst AXIS = { x: [\"left\", undefined, \"right\"], y: [\"up\", undefined, \"down\"] } as const;\n\n/** Everything an indicator wearing the `glide` class needs (utilities.css): its place, whether it may move yet, and\n *  which way it is going, so the edge on that side leads and the other follows: it stretches toward the new choice and\n *  gathers there. */\nexport function glideProps(box: IndicatorBox) {\n  return {\n    style: indicatorStyle(box),\n    \"data-ready\": box.ready ? \"\" : undefined,\n    \"data-glide-x\": AXIS.x[box.dx + 1],\n    \"data-glide-y\": AXIS.y[box.dy + 1],\n  };\n}\n"},{"path":"beamline/hooks/use-motion-tokens.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-motion-tokens.ts","content":"\"use client\";\n\nimport { useMemo, type RefObject } from \"react\";\nimport { useAppearanceRoot } from \"../runtime/appearance\";\nimport { useResolvedAppearance } from \"./use-resolved-appearance\";\nimport { motionAt, motionEase, motionTokens, type MotionTokens } from \"../lib/motion-tokens\";\nimport { semanticMotion } from \"../lib/motion-source\";\n\nconst durationToken = (name: string) =>\n  name === \"exit\" ? \"motion-out\" : name === \"in\" || name === \"out\" ? `motion-${name}` : `duration-${name}`;\nconst easeToken = (name: string) => `ease-${name === \"inOut\" ? \"in-out\" : name}`;\nconst REQUEST = {\n  times: [\n    ...Object.keys(semanticMotion.duration).map(durationToken),\n    ...Object.keys(semanticMotion.spring).map((name) => `spring-${name}`),\n    ...Object.keys(semanticMotion.stagger).map((name) => `stagger-${name}`),\n    \"fade-in\",\n    \"fade-out\",\n  ],\n  easings: [...Object.keys(semanticMotion.ease).map(easeToken), \"ease-spring\", \"ease-spring-bounce\", \"ease-overshoot\"],\n  numbers: [\"motion-scale\", \"motion-look\", \"press-scale\"],\n  // timing only: the host's own type would make every control resolve differently from the page around it\n  typography: false,\n};\n\n/**\n * Resolve each timing at the actual host; omitted host uses the nearest managed root. The first/server snapshot is\n * the pure foundation default. Local CSS values, scope changes, font readiness, OS preference and explicit refresh\n * update the same consumer; no document-wide first-root fallback or persistent global listener.\n */\nexport function useMotionTokens(hostRef?: RefObject<HTMLElement | null>): MotionTokens & {\n  ready: boolean;\n  /** Reduced or none: movement, springs and staggers are instant. */\n  reduced: boolean;\n  mode: \"full\" | \"reduced\" | \"none\";\n  /** Opacity-only arrival and departure (seconds): kept under reduced, instant under none (APPEARANCE A06). */\n  fade: { in: number; out: number };\n} {\n  const root = useAppearanceRoot();\n  const scopeRef = useMemo(() => ({ current: root }), [root]);\n  const resolved = useResolvedAppearance(hostRef ?? scopeRef, REQUEST);\n  return useMemo(() => {\n    if (!resolved.ready)\n      return {\n        ...motionTokens,\n        ready: false,\n        reduced: false,\n        mode: \"full\" as const,\n        fade: { in: motionTokens.duration.in, out: motionTokens.duration.out },\n      };\n    const reduced = resolved.reducedMotion;\n    const speed = (resolved.numbers[\"motion-scale\"] ?? 1) * (resolved.numbers[\"motion-look\"] ?? 1);\n    const fade =\n      resolved.motionMode === \"none\"\n        ? { in: 0.001, out: 0.001 }\n        : {\n            in: resolved.times[\"fade-in\"] ?? semanticMotion.duration.in * speed,\n            out: resolved.times[\"fade-out\"] ?? semanticMotion.duration.out * speed,\n          };\n    const factor = reduced ? 0.001 / semanticMotion.duration.standard : speed;\n    const base = motionAt(factor);\n    const duration = Object.fromEntries(\n      Object.entries(base.duration).map(([name, fallback]) => [\n        name,\n        reduced ? 0.001 : (resolved.times[durationToken(name)] ?? fallback),\n      ]),\n    ) as MotionTokens[\"duration\"];\n    const stagger = Object.fromEntries(\n      Object.entries(base.stagger).map(([name, fallback]) => [\n        name,\n        reduced ? 0 : (resolved.times[`stagger-${name}`] ?? fallback),\n      ]),\n    ) as MotionTokens[\"stagger\"];\n    const ease = Object.fromEntries(\n      Object.entries(base.ease).map(([name, fallback]) => [\n        name,\n        motionEase(resolved.easings[easeToken(name)], fallback),\n      ]),\n    ) as MotionTokens[\"ease\"];\n    const spring = { ...base.spring };\n    for (const name of Object.keys(semanticMotion.spring) as (keyof typeof semanticMotion.spring)[])\n      spring[name] = {\n        ...base.spring[name],\n        visualDuration: reduced ? 0.001 : (resolved.times[`spring-${name}`] ?? base.spring[name].visualDuration),\n        bounce: reduced ? 0 : base.spring[name].bounce,\n      };\n    return {\n      ...base,\n      ready: true,\n      reduced,\n      mode: resolved.motionMode,\n      fade,\n      duration,\n      stagger,\n      ease,\n      spring,\n      pressScale: resolved.numbers[\"press-scale\"] ?? base.pressScale,\n      css: {\n        spring: resolved.easings[\"ease-spring\"] ?? base.css.spring,\n        springBounce: resolved.easings[\"ease-spring-bounce\"] ?? base.css.springBounce,\n        overshoot: resolved.easings[\"ease-overshoot\"] ?? base.css.overshoot,\n      },\n    };\n  }, [resolved]);\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":"Table of contents is in src/ui/beamline/components/table-of-contents/table-of-contents.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/table-of-contents"}}