{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"multi-select","type":"registry:component","title":"Multi-select","description":"Several values from a known list as removable chips in the field; the list has a search field (for long lists), checkable rows and a count with Clear. With sortable, the chips keep the chosen order and move by drag or Alt+← / Alt+→ from a chip's ×, the move announced.","categories":["forms"],"dependencies":["@radix-ui/react-direction@1.1.4","@radix-ui/react-label@2.1.15","@radix-ui/react-popover@1.1.23","@radix-ui/react-slot@1.3.3","@radix-ui/react-tooltip@1.2.16","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/_shared/chip.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/chip.tsx","content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { focusAfterRemoval } from \"../../lib/focus\";\nimport { IconAction } from \"./icon-action\";\n\n/** A value inside a field (a tag, a chosen option) with its remove button. */\nexport function FieldChip({\n  children,\n  onRemove,\n  removeLabel,\n  active,\n  disabled,\n  invalid,\n  className,\n  textClassName,\n  removeClassName,\n  flip,\n  fit,\n}: {\n  children: ReactNode;\n  onRemove?: () => void;\n  removeLabel: string;\n  active?: boolean;\n  disabled?: boolean;\n  invalid?: boolean;\n  className?: string;\n  textClassName?: string;\n  removeClassName?: string;\n  /** A stable key: the chip grows in, slides and leaves a ghost inside a container that runs useFlip. */\n  flip?: string;\n  /** One of a row's items that useFit counts (the row folds what does not fit into \"+N\"). */\n  fit?: boolean;\n}) {\n  return (\n    <span\n      data-slot=\"field-chip\"\n      data-flip={flip}\n      data-fit-item={fit ? \"\" : undefined}\n      data-active={active ? \"\" : undefined}\n      className={cn(\n        \"inline-flex h-6 max-w-full items-center gap-1 rounded-chip bg-hover-2 ps-2 text-xs text-ink\",\n        !onRemove && \"pe-2\",\n        active && \"shadow-[inset_0_0_0_1px_var(--pui-accent)]\",\n        // organism-allow: tone-map-copy -- the field's invalid state, not a tone table\n        invalid && \"bg-red-tint text-red\",\n        className,\n      )}\n    >\n      <span data-slot=\"field-chip-text\" className={cn(\"truncate\", textClassName)}>\n        {children}\n      </span>\n      {onRemove && (\n        <IconAction\n          data-slot=\"field-chip-remove\"\n          size=\"xs\"\n          tabIndex={-1}\n          disabled={disabled}\n          aria-label={removeLabel}\n          onClick={(e) => {\n            e.stopPropagation();\n            const chip = e.currentTarget.closest(\"[data-slot=field-chip]\");\n            focusAfterRemoval(chip, { scope: chip?.closest(\"[data-slot=field-control]\") });\n            onRemove();\n          }}\n          className={removeClassName}\n        >\n          <X className=\"size-3\" />\n        </IconAction>\n      )}\n    </span>\n  );\n}\n"},{"path":"beamline/components/_shared/field.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/field.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A field: label (with an optional aside at the end of its row), the control, then one message line. The gap under\n   the control belongs to the message (--pui-field-gap), so a message that arrives or leaves takes its gap with it. */\n  /* a column, not a grid: a message (or a fold) that leaves pulls its gap away with a negative margin, which a flex\n     column honours and a grid track does not (it would stop at zero and stretch the leaving part back open) */\n  .field {\n    --pui-field-gap: var(--pui-space-1-5);\n    /* positioned: a hidden label or a count's spoken words (sr-only, absolutely placed) stay inside the field */\n    position: relative;\n    display: flex;\n    min-width: 0;\n    flex-direction: column;\n    gap: var(--pui-field-gap);\n  }\n  .labelRow {\n    display: flex;\n    min-width: 0;\n    align-items: baseline;\n    gap: var(--pui-space-2);\n  }\n  .aside {\n    margin-inline-start: auto;\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n  }\n\n  /* The message: one line at a time. A new line grows in to its own height (0fr to 1fr, no measuring) while the one it\n   replaces folds away above it, so the height glides from one to the other; a line that leaves with nothing after it\n   takes the gap with it. Arrival over --pui-motion-in, departure over --pui-motion-out; reduced motion keeps the fades. */\n  .messages {\n    display: grid;\n    margin-top: calc(var(--pui-field-gap, var(--pui-space-1-5)) * -1);\n  }\n  .line {\n    display: grid;\n    grid-template-rows: 1fr;\n    opacity: 1;\n    transition:\n      grid-template-rows var(--pui-motion-in) var(--pui-ease-default),\n      opacity var(--pui-fade-in, var(--pui-motion-in)) var(--pui-ease-default);\n  }\n  @starting-style {\n    .line {\n      grid-template-rows: 0fr;\n      opacity: 0;\n    }\n  }\n  /* the line a field is first drawn with does not grow in */\n  .line[data-initial] {\n    transition: none;\n  }\n  /* a swap is one change of size: the old line folds and the new one grows over the same duration and curve, so their\n     sum (the field's height) moves straight from one to the other, with no swell between; the old words fade first */\n  .line[data-leaving] {\n    grid-template-rows: 0fr;\n    opacity: 0;\n    transition:\n      grid-template-rows var(--pui-motion-out) var(--pui-ease-default),\n      opacity var(--pui-fade-in, var(--pui-motion-in)) var(--pui-ease-default);\n    pointer-events: none;\n  }\n  .messages:has(> .line[data-leaving]) > .line:not([data-leaving], [data-initial]) {\n    transition-duration: var(--pui-motion-out), var(--pui-fade-in, var(--pui-motion-in));\n  }\n  .clip {\n    min-height: 0;\n    overflow: clip;\n    overflow-clip-margin: var(--pui-space-1);\n  }\n  .message {\n    display: flex;\n    align-items: flex-start;\n    gap: var(--pui-space-1-5);\n    padding-top: var(--pui-field-gap, var(--pui-space-1-5));\n    font-size: var(--pui-text-xs);\n    line-height: 1.375;\n  }\n  .description {\n    display: block;\n    color: var(--pui-ink-3);\n  }\n  .error {\n    color: var(--pui-red);\n  }\n  .success {\n    color: var(--pui-green);\n  }\n  .icon {\n    flex-shrink: 0;\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n    margin-top: 1px;\n  }\n\n  /* The count in the label's row: tabular, quiet, shown with attention (.reveal) and in words near the limit. */\n  .count {\n    font-variant-numeric: tabular-nums;\n    white-space: nowrap;\n  }\n  .count[data-state=\"near\"],\n  .count[data-state=\"full\"] {\n    color: var(--pui-orange);\n  }\n  .count[data-state=\"over\"] {\n    color: var(--pui-red);\n  }\n\n  /* The status at the end of the frame: the spinner arrives scaled in from its centre, and closes into the check. */\n  .status {\n    display: inline-grid;\n    flex-shrink: 0;\n    place-items: center;\n    animation: status-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  @keyframes status-in {\n    from {\n      opacity: 0;\n      scale: calc(1 - 0.4 * var(--pui-motion-travel, 1));\n    }\n  }\n\n  /* The one frame every text-like control sits in: input, textarea, password, search, number, the select and date\n   triggers. Its paint (fill, edge, hover, focus) is the field role of the look in force (looks.css); invalid,\n   read-only and disabled are drawn here so they read the same in every look. */\n  .frame {\n    --_field-height: var(--pui-control-height-md);\n    position: relative;\n    display: flex;\n    min-width: 0; /* a grid or flex parent may make it narrower than the input's natural width */\n    align-items: center;\n    gap: var(--pui-space-2);\n    min-height: var(--_field-height);\n    padding-inline: var(--pui-space-2-5);\n    border-radius: var(--pui-field-radius, var(--pui-radius-control));\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    cursor: text;\n    transition:\n      box-shadow var(--pui-motion-out) var(--pui-ease-default),\n      background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .frame:hover:not([data-disabled]) {\n    transition-duration: 0ms;\n  }\n  .frame[data-state=\"open\"] {\n    box-shadow: var(--m-field-edge-focus);\n  }\n  .frame:focus-within {\n    transition: none;\n  }\n  .frame[data-disabled] {\n    opacity: var(--pui-opacity-disabled);\n    cursor: not-allowed;\n  }\n  .frame[data-readonly] {\n    --m-field-bg: transparent;\n  }\n  .sm {\n    --_field-height: var(--pui-control-height-sm);\n    padding-inline: var(--pui-space-2);\n  }\n  .lg {\n    --_field-height: var(--pui-control-height-lg);\n    padding-inline: var(--pui-space-3);\n    font-size: var(--pui-text-base);\n  }\n  .multiline {\n    align-items: stretch;\n    padding-block: var(--pui-space-2);\n  }\n\n  /* The control itself: borderless, it takes the frame's look. Focus shows on the frame, so the control's own ring is off\n   (the frame's ring is the visible focus indicator, which is always present when the control has focus). */\n  .control {\n    flex: 1;\n    min-width: 0;\n    height: 100%;\n    min-height: calc(var(--_field-height) - 2px);\n    border: 0;\n    outline: none;\n    background: transparent;\n    color: inherit;\n    font: inherit;\n    letter-spacing: inherit;\n    padding: 0;\n  }\n  .control:focus-visible {\n    outline: none;\n  }\n  .control::placeholder {\n    color: var(--pui-ink-3);\n    opacity: 1;\n  }\n  .control:disabled {\n    cursor: not-allowed;\n  }\n  textarea.control {\n    min-height: 0;\n    resize: none;\n    line-height: 1.5;\n    field-sizing: content;\n  }\n  .adornment {\n    display: inline-flex;\n    align-items: center;\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n  }\n  .adornment :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.frame {\n  composes: m-field nest-corner from global;\n}\n"},{"path":"beamline/components/_shared/field.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/field.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type CSSProperties,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n  type Ref,\n  type TransitionEvent,\n} from \"react\";\nimport { AlertCircle, CircleCheck } from \"lucide-react\";\nimport { Label } from \"../label/label\";\nimport { Spinner } from \"../spinner/spinner\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./field.module.css\";\n\nexport type FieldSize = \"sm\" | \"md\" | \"lg\";\nexport type FieldClassNames = Partial<\n  Record<\"root\" | \"label\" | \"frame\" | \"leading\" | \"trailing\" | \"description\" | \"error\" | \"success\" | \"aside\", string>\n>;\n\nexport interface FieldOptions {\n  /** Visible label, also the accessible name. */\n  label?: ReactNode;\n  /** Keep the label for assistive tech only (dense rows, toolbars). */\n  hideLabel?: boolean;\n  /** One line of help under the control. */\n  description?: ReactNode;\n  /** The problem and its fix; replaces the description and marks the control invalid. */\n  error?: ReactNode;\n  /** The confirmation that the value works (\"@mira is available\"): replaces the description, with a check. */\n  success?: ReactNode;\n  required?: boolean;\n  size?: FieldSize;\n}\n\n/** Ids and aria wiring for one field: the control gets these spread onto it. */\nexport function useFieldIds(\n  id?: string,\n  { description, error, success }: Pick<FieldOptions, \"description\" | \"error\" | \"success\"> = {},\n) {\n  const auto = useId();\n  const controlId = id ?? `field-${auto}`;\n  // the line under the field is one of three, and the control points at whichever is there\n  const descriptionId = (success || description) && !error ? `${controlId}-description` : undefined;\n  const errorId = error ? `${controlId}-error` : undefined;\n  return {\n    controlId,\n    descriptionId,\n    errorId,\n    aria: {\n      id: controlId,\n      \"aria-describedby\": [descriptionId, errorId].filter(Boolean).join(\" \") || undefined,\n      \"aria-invalid\": error ? true : undefined,\n      \"aria-errormessage\": errorId,\n    },\n  };\n}\n\n/** Label above (with an optional aside at the end of its row: a counter, a link), control frame, then the message. */\nexport function Field({\n  label,\n  hideLabel,\n  description,\n  error,\n  success,\n  required,\n  controlId,\n  descriptionId,\n  errorId,\n  aside,\n  className,\n  classNames,\n  style,\n  slot = \"field\",\n  requiredOnControl,\n  children,\n}: FieldOptions & {\n  controlId: string;\n  descriptionId?: string;\n  errorId?: string;\n  /** At the end of the label's row: a character count, \"Forgot password?\", \"Optional\". */\n  aside?: ReactNode;\n  className?: string;\n  classNames?: FieldClassNames;\n  style?: CSSProperties;\n  slot?: string;\n  /** The control carries `required` itself, so the label does not repeat it in the control's name. */\n  requiredOnControl?: boolean;\n  children: ReactNode;\n}) {\n  const labelNode = label != null && (\n    <Label\n      htmlFor={controlId}\n      required={required}\n      requiredOnControl={requiredOnControl}\n      className={cn(\"text-label\", hideLabel && \"sr-only\", classNames?.label)}\n    >\n      {label}\n    </Label>\n  );\n  return (\n    <div\n      data-slot={slot}\n      data-invalid={error ? \"\" : undefined}\n      style={style}\n      className={cn(styles.field, aside != null && \"reveal-host\", classNames?.root, className)}\n    >\n      {aside != null ? (\n        <div data-slot=\"field-label-row\" className={styles.labelRow}>\n          {labelNode}\n          <span data-slot=\"field-aside\" className={cn(styles.aside, classNames?.aside)}>\n            {aside}\n          </span>\n        </div>\n      ) : (\n        labelNode\n      )}\n      {children}\n      <FieldMessage\n        error={error}\n        success={success}\n        description={description}\n        errorId={errorId}\n        descriptionId={descriptionId}\n        classNames={classNames}\n      />\n    </div>\n  );\n}\n\ntype Tone = \"error\" | \"success\" | \"description\";\ninterface Line {\n  key: string;\n  tone: Tone;\n  content: ReactNode;\n}\nconst ICON = { error: AlertCircle, success: CircleCheck } as const;\n\n/**\n * The line under a field: its problem when there is one, else its confirmation, else its help. Shared by every field\n * and choice group. A change of line keeps its path: the new line grows in under the control while the old one folds\n * away above it, so what sits below glides instead of jumping, and a quick reversal turns around from where it is.\n * The leaving line is a still copy, hidden from assistive tech and inert; ids and aria point only at the line that is\n * there. It is gone when its fold ends, and by the clock as well: a line that leaves in the frame it arrived has no\n * fold to run, so no end is reported.\n */\nexport function FieldMessage({\n  error,\n  success,\n  description,\n  errorId,\n  descriptionId,\n  slot = \"field\",\n  classNames,\n}: Pick<FieldOptions, \"error\" | \"description\" | \"success\"> & {\n  errorId?: string;\n  descriptionId?: string;\n  /** The slot names' prefix: \"checkbox\" names the lines checkbox-error, checkbox-description. */\n  slot?: string;\n  classNames?: Pick<FieldClassNames, \"error\" | \"description\" | \"success\">;\n}) {\n  const tone: Tone | null = error ? \"error\" : success ? \"success\" : description ? \"description\" : null;\n  const content = error || success || description;\n  // a new text is a new line (one problem replacing another also folds and grows); rich content changes in place\n  const text = typeof content === \"string\" || typeof content === \"number\" ? String(content) : \"\";\n  const current: Line | null = tone ? { key: `${tone}\\u0000${text}`, tone, content } : null;\n  const [last, setLast] = useState<Line | null>(current);\n  const [leaving, setLeaving] = useState<Line | null>(null);\n  // the line a field is drawn with is simply there; only a change of line moves\n  const [changed, setChanged] = useState(false);\n  if ((last?.key ?? null) !== (current?.key ?? null)) {\n    // the line that was there leaves (one at a time: a line already leaving is gone at once)\n    setLeaving(last && last.key !== current?.key ? last : null);\n    setLast(current);\n    setChanged(true);\n  }\n  const box = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    if (!leaving) return;\n    // the clock is the leaving line's own longest transition, read once as it starts to leave (the fold's end event\n    // usually comes first), so a field resolves no motion tokens of its own\n    const el = box.current?.querySelector<HTMLElement>(\"[data-leaving]\");\n    const longest = el\n      ? Math.max(\n          0,\n          ...getComputedStyle(el)\n            .transitionDuration.split(\",\")\n            .map((t) => parseFloat(t) * (t.trim().endsWith(\"ms\") ? 1 : 1000) || 0),\n        )\n      : 0;\n    const id = window.setTimeout(() => setLeaving((l) => (l === leaving ? null : l)), longest + 250);\n    return () => window.clearTimeout(id);\n  }, [leaving]);\n  if (!current && !leaving) return null;\n  const shown = leaving && leaving.key === current?.key ? null : leaving;\n  const ids = { error: errorId, success: descriptionId, description: descriptionId };\n  const line = (l: Line, gone: boolean) => {\n    const Icon = l.tone === \"description\" ? null : ICON[l.tone];\n    return (\n      <div\n        key={l.key}\n        data-slot=\"field-message-line\"\n        data-leaving={gone ? \"\" : undefined}\n        data-initial={changed ? undefined : \"\"}\n        aria-hidden={gone || undefined}\n        inert={gone || undefined}\n        className={styles.line}\n        onTransitionEnd={\n          gone\n            ? (e: TransitionEvent<HTMLDivElement>) => {\n                if (e.target === e.currentTarget && e.propertyName === \"grid-template-rows\") setLeaving(null);\n              }\n            : undefined\n        }\n      >\n        <div className={styles.clip}>\n          <p\n            id={gone ? undefined : ids[l.tone]}\n            data-slot={`${slot}-${l.tone}`}\n            className={cn(styles.message, styles[l.tone], classNames?.[l.tone])}\n          >\n            {Icon && <Icon aria-hidden className={styles.icon} />}\n            {l.content}\n          </p>\n        </div>\n      </div>\n    );\n  };\n  return (\n    <div ref={box} data-slot=\"field-message\" className={styles.messages}>\n      {shown && line(shown, true)}\n      {current && line(current, false)}\n    </div>\n  );\n}\n\n/** What the end of a text field says about its work: a spinner while a value is checked, a drawn check when it works. */\nexport type FieldStatus = \"busy\" | \"success\";\n\n/** The frame a text-like control sits in; clicking anywhere on it focuses the control. */\nexport function FieldFrame({\n  size = \"md\",\n  invalid,\n  disabled,\n  readOnly,\n  multiline,\n  status,\n  statusLabel: statusLabelProp,\n  leading,\n  trailing,\n  className,\n  classNames,\n  children,\n  onPointerDown,\n  ref,\n}: {\n  size?: FieldSize;\n  invalid?: boolean;\n  disabled?: boolean;\n  readOnly?: boolean;\n  multiline?: boolean;\n  /** busy: a spinner at the end while the value is checked or saved; success: the ring closes into a check. */\n  status?: FieldStatus;\n  /** What the spinner says while busy (\"Checking\", \"Saving\"). */\n  statusLabel?: string;\n  leading?: ReactNode;\n  trailing?: ReactNode;\n  className?: string;\n  classNames?: FieldClassNames;\n  children: ReactNode;\n  onPointerDown?: (e: ReactPointerEvent<HTMLDivElement>) => void;\n  /** The frame itself, for a control that lays out by the frame's width (useWidthAs). */\n  ref?: Ref<HTMLDivElement>;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const statusLabel = statusLabelProp === undefined ? i18n(\"statusLabel\") : statusLabelProp;\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"field-control\"\n      data-invalid={invalid ? \"\" : undefined}\n      data-disabled={disabled ? \"\" : undefined}\n      data-readonly={readOnly ? \"\" : undefined}\n      data-status={status}\n      className={cn(styles.frame, styles[size], multiline && styles.multiline, classNames?.frame, className)}\n      onPointerDown={(e) => {\n        onPointerDown?.(e);\n        // a click on the frame's padding or an adornment that is not itself interactive focuses the control\n        const t = e.target as HTMLElement;\n        if (t === e.currentTarget || (t.closest(\"[data-adornment]\") && !t.closest(\"button, a, input, [role=button]\"))) {\n          e.preventDefault();\n          e.currentTarget.querySelector<HTMLElement>(\"input, textarea\")?.focus();\n        }\n      }}\n    >\n      {leading && (\n        <span\n          data-slot=\"field-adornment\"\n          data-adornment=\"leading\"\n          className={cn(styles.adornment, classNames?.leading)}\n        >\n          {leading}\n        </span>\n      )}\n      {children}\n      {status && (\n        // one ring for both: it spins while the value is checked and closes into the check when it works\n        <span data-slot=\"field-status\" data-adornment=\"status\" className={styles.status}>\n          <Spinner size=\"sm\" state={status === \"busy\" ? \"loading\" : \"success\"} label={statusLabel} />\n        </span>\n      )}\n      {trailing && (\n        <span\n          data-slot=\"field-adornment\"\n          data-adornment=\"trailing\"\n          className={cn(styles.adornment, classNames?.trailing)}\n        >\n          {trailing}\n        </span>\n      )}\n    </div>\n  );\n}\n\n/**\n * A character count for the label's row (Field `aside`): quiet (\"58/140\") and shown while the field has attention,\n * then always shown and in words as the limit nears (\"12 left\", \"Limit reached\", \"3 over\"), so it never competes with\n * the label at rest. Assistive tech hears it only when it crosses into those words, not at every key.\n */\nexport function FieldCount({\n  length,\n  max,\n  near: nearAt,\n  slot = \"field-count\",\n  className,\n}: {\n  length: number;\n  max: number;\n  /** How many left turns it into words: by default 10 % of the limit, at least 5 (characters); 1 suits a few tags. */\n  near?: number;\n  slot?: string;\n  className?: string;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const left = max - length;\n  const near = left <= (nearAt ?? Math.max(5, Math.ceil(max * 0.1)));\n  const state = left < 0 ? \"over\" : left === 0 ? \"full\" : near ? \"near\" : \"room\";\n  const words =\n    state === \"over\"\n      ? i18n(\"fieldOver\", { count: -left })\n      : state === \"full\"\n        ? i18n(\"fieldFull\")\n        : i18n(\"fieldLeft\", { count: left });\n  return (\n    <span\n      data-slot={slot}\n      data-state={state}\n      data-shown={state === \"room\" ? undefined : \"\"}\n      className={cn(\"reveal\", styles.count, className)}\n    >\n      <span aria-hidden>{state === \"room\" ? `${i18n.number(length)}/${i18n.number(max)}` : words}</span>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {state === \"room\" ? \"\" : words}\n      </span>\n    </span>\n  );\n}\n\n/** The class a native input or textarea needs inside a FieldFrame. */\nexport const fieldControlClass = styles.control;\n\n/** What a choice group (segmented control, radio cards) spreads onto its own root when it is a form field. */\nexport interface ChoiceFieldAria {\n  \"aria-labelledby\": string;\n  \"aria-describedby\"?: string;\n  \"aria-invalid\"?: true;\n}\n\n/**\n * Whether a choice control (segmented control, radio cards, chip group) is laid out as a form field, its label shown\n * above like every field's: when asked (`field`), when it has a description, error or required mark, and whenever its\n * label is not hidden. A view switch or a quick filter passes `hideLabel`: its label stays for assistive tech only.\n * (Trials t16 and t19: agents wrote <SegmentedControl label=\"Catering needed\" …> and the question never showed.)\n */\nexport function choiceIsField(\n  field: boolean | undefined,\n  label: ReactNode,\n  hideLabel: boolean | undefined,\n  marked: boolean,\n) {\n  return field ?? (marked || (label != null && label !== \"\" && !hideLabel));\n}\n\n/**\n * A choice group as a form field: the visible label above (with the required marker), the group, then its help or its\n * problem — the same frame and message every text field has. Groups that name themselves only for assistive tech\n * (a view switch in a toolbar) do not use it.\n */\nexport function ChoiceField({\n  slot,\n  label,\n  hideLabel,\n  required,\n  description,\n  error,\n  className,\n  style,\n  classNames,\n  requiredOnControl,\n  children,\n}: Omit<FieldOptions, \"size\" | \"success\"> & {\n  slot: string;\n  /** The group carries `aria-required` itself (a radiogroup), so the label does not repeat it in the group's name. */\n  requiredOnControl?: boolean;\n  className?: string;\n  style?: CSSProperties;\n  classNames?: Pick<FieldClassNames, \"label\" | \"description\" | \"error\">;\n  children: (aria: ChoiceFieldAria) => ReactNode;\n}) {\n  const auto = useId();\n  const labelId = `choice-${auto}-label`;\n  const messageId = error || description ? `choice-${auto}-message` : undefined;\n  return (\n    <div data-slot={slot} data-invalid={error ? \"\" : undefined} style={style} className={cn(styles.field, className)}>\n      <Label\n        id={labelId}\n        required={required}\n        requiredOnControl={requiredOnControl}\n        className={cn(\"text-label\", hideLabel && \"sr-only\", classNames?.label)}\n      >\n        {label}\n      </Label>\n      {children({\n        \"aria-labelledby\": labelId,\n        ...(messageId ? { \"aria-describedby\": messageId } : {}),\n        ...(error ? { \"aria-invalid\": true as const } : {}),\n      })}\n      <FieldMessage\n        error={error}\n        description={description}\n        errorId={messageId}\n        descriptionId={messageId}\n        classNames={classNames}\n      />\n    </div>\n  );\n}\n"},{"path":"beamline/components/_shared/fold-more.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/fold-more.tsx","content":"\"use client\";\n// organism-allow-file: raw-control -- the shared +N part itself, painted by its owner's look\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\n\nimport { useLayoutEffect, useRef, type PointerEventHandler } from \"react\";\nimport { Tooltip } from \"../tooltip/tooltip\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { rollText } from \"./roll\";\nimport { resolveCssEasing } from \"../../lib/css-values\";\nimport { cn } from \"../../lib/utils\";\n\n/** The most names the tooltip lists before it counts the rest. */\nconst NAMED = 8;\n/** A fold that has just appeared settles its count without a roll: the row measures the \"+N\" once it is on screen and\n *  may correct the count right after (the room did not change, so nothing should travel). */\nconst SETTLING_MS = 400;\n\n/**\n * The \"+N\" a folded row ends in (hooks/use-fit.ts): one part for every row of chips, tags, filters or sources, so a\n * fold reads and behaves the same in each. It counts what did not fit and names those items in the system tooltip on\n * hover or focus; with `onExpand` it is a button that shows them all (\"Show 3 more filters: …\" to a screen reader),\n * without it a quiet mark for a field that brings its items back on its own (a tag field on focus). When the room\n * changes, the count rolls to its new value: up as more fold, down as they come back (a fold that has just appeared\n * settles its first count in place). The row gives it its geometry through `className` (height, radius, padding), so\n * it stands like its neighbours; the look and the behaviour are the part's.\n */\nexport function FoldMore({\n  names,\n  noun,\n  onExpand,\n  onPointerDown,\n  marks = true,\n  hidden,\n  slot,\n  className,\n}: {\n  /** The folded items, in order: their count is shown; their names are the tooltip and the spoken label. */\n  names: string[];\n  /** What the items are, for the spoken label (\"sources\", \"filters\"). */\n  noun?: string;\n  /** Show every folded item (the row expands, a menu opens). Without it the mark is not a control. */\n  onExpand?: () => void;\n  onPointerDown?: PointerEventHandler<HTMLElement>;\n  /** Mark it as the row's overflow control for useFit and useFlip (`data-fit-more`, `data-flip=\"more\"`). Off when a\n   *  wrapper of the caller's is the row's child and carries the marks (a list item). Default true. */\n  marks?: boolean;\n  /** Hidden from assistive tech: the control it sits in already names every item (a multi-select trigger). */\n  hidden?: boolean;\n  /** The owner's part name (data-slot). */\n  slot?: string;\n  className?: string;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const count = useRef<HTMLSpanElement>(null);\n  const motion = useMotionTokens(count);\n  const last = useRef(names.length);\n  const born = useRef<number | null>(null);\n  useLayoutEffect(() => {\n    born.current ??= performance.now();\n    const was = last.current;\n    last.current = names.length;\n    const el = count.current;\n    if (!el || was === names.length || motion.reduced || performance.now() - born.current < SETTLING_MS) return;\n    rollText(el, `+${i18n.number(was)}`, `+${i18n.number(names.length)}`, names.length > was ? 1 : -1, {\n      duration: motion.duration.standard * 1000,\n      easing: resolveCssEasing(el, \"--pui-ease-default\"),\n    });\n  }, [names.length, i18n]); // eslint-disable-line react-hooks/exhaustive-deps -- rolls on a change of the count only\n  const listed =\n    names.length > NAMED\n      ? i18n(\"foldMore\", { list: i18n.list(names.slice(0, NAMED)), count: names.length - NAMED })\n      : i18n.list(names);\n  const marked = marks ? { \"data-fit-more\": \"\", \"data-flip\": \"more\" } : {};\n  const look = cn(\n    \"relative inline-flex h-6 shrink-0 items-center rounded-chip px-2 text-xs text-ink-2 tabular-nums\",\n    \"shadow-[inset_0_0_0_1px_var(--pui-line)]\",\n  );\n  const body = (\n    <span ref={count} data-slot={slot ? `${slot}-count` : undefined}>\n      +{i18n.number(names.length)}\n    </span>\n  );\n  return (\n    <Tooltip content={listed}>\n      {onExpand ? (\n        <button\n          type=\"button\"\n          data-slot={slot}\n          {...marked}\n          aria-label={i18n(\"foldShow\", { count: names.length, noun: noun ?? \"\", list: i18n.list(names) })}\n          onClick={onExpand}\n          onPointerDown={onPointerDown}\n          className={cn(look, \"ix hover:bg-hover hover:text-ink\", className)}\n        >\n          {body}\n        </button>\n      ) : (\n        <span\n          data-slot={slot}\n          {...marked}\n          aria-hidden={hidden || undefined}\n          onPointerDown={onPointerDown}\n          className={cn(look, className)}\n        >\n          {body}\n        </span>\n      )}\n    </Tooltip>\n  );\n}\n"},{"path":"beamline/components/_shared/glide.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/glide.tsx","content":"\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type RefObject } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport overlay from \"./overlay.module.css\";\n\n/**\n * The gliding highlight for a list whose active row is tracked in state (command menu, listbox): one plate travels to\n * the active row for the arrow keys and the pointer alike, over --pui-duration-fast. It appears in place (no travel)\n * the first time and whenever `reset` changes (a new page or a new query), so typing never sends it flying.\n * The list must be the plate's offset parent (position: relative); rows stay above it (overlay.item is z-index 1).\n */\nexport function GlidePlate({\n  list,\n  selector,\n  reset,\n  axis = \"y\",\n  className,\n}: {\n  list: RefObject<HTMLElement | null>;\n  /** Finds the active row inside `list`; null when nothing is active. */\n  selector: string | null;\n  reset?: unknown;\n  /** \"y\" for a list (the plate spans the width); \"xy\" for a row or grid of cells (it takes each cell's size). */\n  axis?: \"y\" | \"xy\";\n  className?: string;\n}) {\n  const [plate, setPlate] = useState<{ x: number; y: number; w: number; h: number; jump: boolean } | null>(null);\n  const lastReset = useRef(reset);\n  const self = useRef<HTMLSpanElement>(null);\n  // eslint-disable-next-line react-hooks/exhaustive-deps -- every render on purpose; setPlate bails out when unchanged\n  useLayoutEffect(() => {\n    // the list holds the plate, and React attaches a parent's ref only after its children's layout effects: on the\n    // first commit the list is the plate's own parent (it placed itself only when something rendered again later)\n    const host = list.current ?? self.current?.parentElement;\n    const row = selector ? host?.querySelector<HTMLElement>(selector) : null;\n    const fresh = lastReset.current !== reset;\n    lastReset.current = reset;\n    if (!row) {\n      setPlate((p) => (p && p.h ? { ...p, h: 0 } : p));\n      return;\n    }\n    const x = row.offsetLeft,\n      y = row.offsetTop,\n      w = row.offsetWidth,\n      h = row.offsetHeight;\n    setPlate((p) =>\n      p && p.x === x && p.y === y && p.w === w && p.h === h && !fresh ? p : { x, y, w, h, jump: fresh || !p?.h },\n    );\n    // every render: rows can arrive or change height without the selector changing; an unchanged place keeps the state\n  });\n  return (\n    <span\n      ref={self}\n      aria-hidden\n      data-slot=\"list-highlight\"\n      className={cn(overlay.glide, className)}\n      data-on={plate?.h ? \"\" : undefined}\n      data-jump={plate?.jump ? \"\" : undefined}\n      data-axis={axis}\n      style={\n        !plate\n          ? undefined\n          : axis === \"y\"\n            ? { transform: `translateY(${plate.y}px)`, height: plate.h || undefined }\n            : { transform: `translate(${plate.x}px, ${plate.y}px)`, width: plate.w, height: plate.h || undefined }\n      }\n    />\n  );\n}\n\n/**\n * A GlidePlate for lists whose highlight is the pointer or keyboard focus rather than state (time picker, plain option\n * lists): it follows the row under the pointer or holding focus, and fades when neither is in the list. Rows drop\n * their own hover and focus tint.\n */\nexport function FollowPlate({\n  list,\n  rows,\n  className,\n}: {\n  list: RefObject<HTMLElement | null>;\n  /** Matches a row inside `list`, e.g. \"[role=option]\". */\n  rows: string;\n  className?: string;\n}) {\n  const [hot, setHot] = useState<string | null>(null);\n  // after the commit, when the list's ref is attached (a layout effect here ran before it and never listened)\n  useEffect(() => {\n    const el = list.current;\n    if (!el) return;\n    let n = 0;\n    const mark = (target: EventTarget | null) => {\n      const row = (target as HTMLElement | null)?.closest?.<HTMLElement>(rows);\n      if (!row || !el.contains(row)) return;\n      row.dataset.glideKey ??= String(++n);\n      setHot(row.dataset.glideKey);\n    };\n    const over = (e: Event) => mark(e.target);\n    const leave = () => {\n      if (!el.contains(document.activeElement)) setHot(null);\n    };\n    const blur = (e: FocusEvent) => {\n      if (!el.contains(e.relatedTarget as Node)) setHot(null);\n    };\n    el.addEventListener(\"pointerover\", over);\n    el.addEventListener(\"focusin\", over);\n    el.addEventListener(\"pointerleave\", leave);\n    el.addEventListener(\"focusout\", blur);\n    return () => {\n      el.removeEventListener(\"pointerover\", over);\n      el.removeEventListener(\"focusin\", over);\n      el.removeEventListener(\"pointerleave\", leave);\n      el.removeEventListener(\"focusout\", blur);\n    };\n  }, [list, rows]);\n  return <GlidePlate list={list} selector={hot ? `[data-glide-key=\"${hot}\"]` : null} className={className} />;\n}\n"},{"path":"beamline/components/_shared/icon-action.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/icon-action.module.css","content":"@layer properties, theme, base, components, utilities;\n/* The small square action that sits inside another element: a field's clear and open buttons, a chip's remove, a\n   toast's close, a carousel's previous and next. One recipe so every one of them answers alike: quiet ink at rest,\n   the hover plate at once, the press at once, both let go over --pui-motion-out. Its corner is the chip radius: under\n   the nested-corner rule (.nest-corner) a part whose own corner is the chip never changes inside a rounded frame. */\n@layer components {\n  .root {\n    position: relative;\n    display: grid;\n    flex-shrink: 0;\n    place-items: center;\n    inline-size: var(--_size);\n    block-size: var(--_size);\n    border-radius: var(--pui-radius-chip);\n    color: var(--pui-ink-3);\n    /* opacity and translate stay in the list so a .reveal action keeps its own arrival whichever rule wins */\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      transform var(--pui-motion-out) var(--pui-ease-default),\n      opacity var(--pui-motion-out) var(--pui-ease-default),\n      translate var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root:hover:not(:disabled),\n  .root[data-pressed] {\n    background-color: var(--pui-hover);\n    color: var(--pui-ink);\n  }\n  .root:hover {\n    transition-duration: 0ms;\n  }\n  .root:active:not(:disabled) {\n    transform: scale(var(--pui-press-scale));\n    transition-duration: 0ms;\n  }\n  .root:disabled {\n    cursor: default;\n    opacity: 0.4;\n  }\n  .root[data-tone=\"ink-2\"] {\n    color: var(--pui-ink-2);\n  }\n  .root[data-tone=\"inherit\"] {\n    color: inherit;\n  }\n  .root[data-tone=\"inherit\"]:hover:not(:disabled) {\n    color: inherit;\n  }\n  /* on an inverted (ink) surface: inverse ink, a faint inverse plate */\n  .root[data-tone=\"inverse\"] {\n    color: color-mix(in oklab, var(--pui-ink-inverse) 72%, transparent);\n  }\n  .root[data-tone=\"inverse\"]:hover:not(:disabled),\n  .root[data-tone=\"inverse\"][data-pressed] {\n    background-color: color-mix(in oklab, var(--pui-ink-inverse) 12%, transparent);\n    color: var(--pui-ink-inverse);\n  }\n\n  .xs {\n    --_size: 1.25rem;\n  }\n  /* a 20px action still takes a 24px hit (WCAG 2.5.8): the bleed is outside the drawn square */\n  .xs::after {\n    position: absolute;\n    inset: calc((var(--pui-hit-min) - 1.25rem) / -2);\n    content: \"\";\n  }\n  .sm {\n    --_size: 1.5rem;\n  }\n  .md {\n    --_size: 1.75rem;\n  }\n  .lg {\n    --_size: 2rem;\n  }\n}\n"},{"path":"beamline/components/_shared/icon-action.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/icon-action.tsx","content":"\"use client\";\n// organism-allow-file: raw-control -- the shared part every element draws this control from\n\nimport type { ButtonHTMLAttributes, Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./icon-action.module.css\";\n\n/** 20 · 24 · 28 · 32 px. A 20px action keeps a 24px hit outside its square. */\nexport type IconActionSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nexport interface IconActionProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  /** The action's name: the button holds only an icon. */\n  \"aria-label\": string;\n  size?: IconActionSize;\n  /** Rest ink: ink-3 (the default, an action beside content), ink-2 (an action that is the row's point), inverse (on\n   *  an inverted ink surface) or inherit (on a painted surface that brings its own colours). */\n  tone?: \"ink-3\" | \"ink-2\" | \"inverse\" | \"inherit\";\n  /** Holds the hover plate while what it opens is open, or while it is switched on. */\n  pressed?: boolean;\n  ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * The small square icon action inside another element (clear, remove, dismiss, previous, next, retry). One recipe for\n * all of them; a full-size action with words is a Button, and Button with iconOnly is the standalone square.\n */\nexport function IconAction({\n  size = \"sm\",\n  tone = \"ink-3\",\n  pressed,\n  type = \"button\",\n  className,\n  ref,\n  ...props\n}: IconActionProps) {\n  return (\n    <button\n      ref={ref}\n      type={type}\n      data-tone={tone === \"ink-3\" ? undefined : tone}\n      data-pressed={pressed ? \"\" : undefined}\n      className={cn(styles.root, styles[size], className)}\n      {...props}\n    />\n  );\n}\n"},{"path":"beamline/components/_shared/listbox.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/listbox.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\n\nimport { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { Highlight, filterRanked } from \"./match\";\nimport { cn } from \"../../lib/utils\";\nimport { GlidePlate } from \"./glide\";\nimport overlay from \"./overlay.module.css\";\n\nexport interface ListOption {\n  value: string;\n  label: string;\n  description?: string;\n  /** Other words that should find it. */\n  keywords?: string[];\n  icon?: ReactNode;\n  disabled?: boolean;\n  /** Heading it sits under (Billing address). With no query the list shows a heading where the group changes, in the\n   * given order; while searching, results rank flat and each row names its group beside its description. */\n  group?: string;\n}\n\n/** Lists longer than this mount only the rows near the visible window. */\nconst WINDOW = 200;\n/** A one-line row's height before it is measured, and rows kept mounted past each edge. */\nconst ROW = 34;\nconst OVERSCAN = 8;\n\n/** Filtering and the active row for a search-as-you-type list; the field keeps focus and points at the active row. */\nexport function useListbox(options: ListOption[], query: string, extra?: ListOption | null) {\n  // a group's name finds its options too; `extra` (a row that acts, such as Create \"…\") always comes last\n  const ranked = useMemo(\n    () => filterRanked(options, query, (o) => (o.group ? { ...o, keywords: [...(o.keywords ?? []), o.group] } : o)),\n    [options, query],\n  );\n  const filtered = useMemo(() => (extra ? [...ranked, extra] : ranked), [ranked, extra]);\n  const [active, setActive] = useState(0);\n  const viaKeys = useRef(false);\n  useEffect(() => {\n    setActive(\n      Math.max(\n        0,\n        filtered.findIndex((o) => !o.disabled),\n      ),\n    );\n  }, [query]); // eslint-disable-line react-hooks/exhaustive-deps -- reset per question\n  const move = (delta: number) => {\n    if (!filtered.length) return;\n    viaKeys.current = true;\n    let i = active;\n    for (let n = 0; n < filtered.length; n++) {\n      i = (i + delta + filtered.length) % filtered.length;\n      if (!filtered[i].disabled) break;\n    }\n    setActive(i);\n  };\n  // Home, End and Page keys: to a place in the whole list (no wrap), the nearest enabled row from there\n  const jump = (to: number) => {\n    if (!filtered.length) return;\n    viaKeys.current = true;\n    const at = Math.max(0, Math.min(filtered.length - 1, to));\n    const back = to < active;\n    for (let n = 0; n < filtered.length; n++) {\n      const i = back ? at + n : at - n;\n      if (i >= 0 && i < filtered.length && !filtered[i].disabled) return setActive(i);\n    }\n  };\n  return {\n    filtered,\n    active: Math.min(active, filtered.length - 1),\n    setActive,\n    move,\n    jump,\n    viaKeys,\n    current: filtered[Math.min(active, filtered.length - 1)],\n  };\n}\n\n/** The list itself: rows in the system's menu look, the match emphasised, chosen rows checked. */\nexport function Listbox({\n  id,\n  label,\n  options,\n  query,\n  active,\n  onActive,\n  isSelected,\n  onPick,\n  multiple,\n  emptyText,\n  viaKeys,\n  className,\n}: {\n  id: string;\n  label: string;\n  options: ListOption[];\n  query: string;\n  active: number;\n  onActive: (i: number) => void;\n  isSelected: (value: string) => boolean;\n  onPick: (option: ListOption) => void;\n  multiple?: boolean;\n  emptyText?: ReactNode;\n  viaKeys: { current: boolean };\n  className?: string;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const list = useRef<HTMLDivElement>(null);\n  // a long list mounts only the rows near the visible window; spacers above and below keep its scroll height true\n  const windowed = options.length > WINDOW;\n  const heights = useRef(new Map<string, number>());\n  const [scroll, setScroll] = useState({ top: 0, height: 300 });\n  const [measured, setMeasured] = useState(0);\n  const offsets = useMemo(() => {\n    if (!windowed) return null;\n    const out = new Float64Array(options.length + 1);\n    for (let i = 0; i < options.length; i++)\n      out[i + 1] = out[i] + (heights.current.get(options[i].value) ?? (options[i].description ? 46 : ROW));\n    return out;\n  }, [windowed, options, measured]); // eslint-disable-line react-hooks/exhaustive-deps -- measured invalidates the cached heights\n  // the first row whose bottom is below y\n  const rowAt = (y: number) => {\n    let lo = 0;\n    let hi = options.length;\n    while (lo < hi) {\n      const mid = (lo + hi) >> 1;\n      if (offsets![mid + 1] <= y) lo = mid + 1;\n      else hi = mid;\n    }\n    return lo;\n  };\n  const first = windowed ? Math.max(0, rowAt(scroll.top) - OVERSCAN) : 0;\n  const last = windowed ? Math.min(options.length, rowAt(scroll.top + scroll.height) + OVERSCAN + 1) : options.length;\n  // the mounted rows measure once each time the window moves; a row only re-renders when a height changed\n  useLayoutEffect(() => {\n    if (!windowed || !list.current) return;\n    let changed = false;\n    for (const el of list.current.querySelectorAll<HTMLElement>(\"[data-row]\")) {\n      const h = el.offsetHeight;\n      if (h && heights.current.get(el.dataset.row!) !== h) {\n        heights.current.set(el.dataset.row!, h);\n        changed = true;\n      }\n    }\n    if (changed) setMeasured((m) => m + 1);\n  }, [windowed, first, last, options]);\n  useEffect(() => {\n    if (!viaKeys.current) return;\n    const row = list.current?.querySelector(`#${CSS.escape(`${id}-${active}`)}`);\n    if (row) row.scrollIntoView({ block: \"nearest\" });\n    else if (offsets && list.current) {\n      // not mounted: scroll its place into view and the row renders there\n      const el = list.current;\n      if (offsets[active] < el.scrollTop) el.scrollTop = offsets[active];\n      else if (offsets[active + 1] > el.scrollTop + el.clientHeight)\n        el.scrollTop = offsets[active + 1] - el.clientHeight;\n    }\n  }, [active, id, viaKeys, offsets]);\n  // what a search found, said once it settles (a polite status beside the list, never inside it: a listbox holds\n  // options only), so a screen reader hears \"3 results\" or that nothing matched while focus stays in the field\n  const found = !query.trim()\n    ? \"\"\n    : options.length\n      ? i18n(\"results\", { count: options.length })\n      : typeof emptyText === \"string\"\n        ? emptyText\n        : i18n(\"noMatch\", { query });\n  return (\n    <>\n      <span role=\"status\" className=\"sr-only\" data-slot=\"listbox-status\">\n        {found}\n      </span>\n      <div\n        ref={list}\n        id={id}\n        role=\"listbox\"\n        aria-label={label}\n        aria-multiselectable={multiple || undefined}\n        onPointerMove={() => {\n          viaKeys.current = false;\n        }}\n        onScroll={\n          windowed\n            ? (e) => setScroll({ top: e.currentTarget.scrollTop, height: e.currentTarget.clientHeight })\n            : undefined\n        }\n        className={cn(\n          \"relative max-h-[min(300px,var(--radix-popover-content-available-height,300px))] overflow-y-auto p-1\",\n          className,\n        )}\n      >\n        <GlidePlate list={list} selector={options.length ? '[role=\"option\"][data-active]' : null} reset={query} />\n        {options.length === 0 && (\n          <div className=\"px-3 py-6 text-center text-sm text-ink-3\">{emptyText ?? i18n(\"noMatch\", { query })}</div>\n        )}\n        {windowed && <div aria-hidden style={{ height: offsets![first] }} data-slot=\"listbox-spacer\" />}\n        {options.slice(first, last).map((o, k) => {\n          const i = first + k;\n          const on = isSelected(o.value);\n          const prev = options[i - 1];\n          // headings only for the unsearched list, where they follow the caller's order; arrow keys never land on them\n          const groupStart = !query && (o.group ?? \"\") !== (prev?.group ?? \"\") && (i > 0 || !!o.group);\n          const detail = query && o.group ? [o.group, o.description].filter(Boolean).join(\" · \") : o.description;\n          const row = (\n            <>\n              {groupStart && i > 0 && (\n                <div role=\"presentation\" data-slot=\"listbox-separator\" className={overlay.separator} />\n              )}\n              {groupStart && o.group && (\n                <div role=\"presentation\" data-slot=\"listbox-group\" className={overlay.heading}>\n                  {o.group}\n                </div>\n              )}\n              <div\n                id={`${id}-${i}`}\n                role=\"option\"\n                aria-selected={on}\n                aria-disabled={o.disabled || undefined}\n                aria-description={!query && o.group ? o.group : undefined}\n                aria-setsize={windowed ? options.length : undefined}\n                aria-posinset={windowed ? i + 1 : undefined}\n                data-disabled={o.disabled ? \"\" : undefined}\n                data-active={i === active ? \"\" : undefined}\n                className={cn(overlay.item, \"cursor-pointer\", detail && \"py-1.5\")}\n                onPointerMove={() => {\n                  if (i !== active && !o.disabled) onActive(i);\n                }}\n                onMouseDown={(e) => e.preventDefault()}\n                onClick={() => {\n                  if (!o.disabled) onPick(o);\n                }}\n              >\n                {/* a checked row fills at once and its check scales in (a fade under reduced motion); unchecking\n                  lets the fill go over --pui-motion-out, like every hover */}\n                {multiple && (\n                  <span\n                    aria-hidden\n                    data-slot=\"listbox-check\"\n                    className={cn(\n                      \"grid size-4 shrink-0 place-items-center rounded-[calc(var(--pui-radius-chip)-2px)] shadow-[inset_0_0_0_1px_var(--pui-line-strong)] transition-[background-color,box-shadow] duration-[var(--pui-motion-out)]\",\n                      on && \"bg-accent text-[var(--pui-accent-foreground)] shadow-none duration-0\",\n                    )}\n                  >\n                    {on && <Check className=\"!size-3 animate-enter-scale !text-current\" strokeWidth={3} />}\n                  </span>\n                )}\n                {o.icon}\n                <span className=\"grid min-w-0 flex-1 gap-px leading-snug\">\n                  <span className=\"truncate\">\n                    <Highlight text={o.label} query={query} />\n                  </span>\n                  {detail && <span className=\"truncate text-xs text-ink-3\">{detail}</span>}\n                </span>\n                {!multiple && on && <Check data-slot=\"listbox-check\" className=\"animate-enter-scale !text-accent\" />}\n              </div>\n            </>\n          );\n          // a windowed row is measured with its heading, so the spacers stay true\n          return windowed ? (\n            <div key={o.value} role=\"presentation\" data-row={o.value}>\n              {row}\n            </div>\n          ) : (\n            <Fragment key={o.value}>{row}</Fragment>\n          );\n        })}\n        {windowed && (\n          <div aria-hidden style={{ height: offsets![options.length] - offsets![last] }} data-slot=\"listbox-spacer\" />\n        )}\n      </div>\n    </>\n  );\n}\n"},{"path":"beamline/components/_shared/match.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/match.tsx","content":"/** One way to rank and mark matches for every search-as-you-type list (command menu, combobox, multi-select). */\n\n/** How well `label` (and its keywords) answers `query`: label start 100, word start 80, anywhere 60, keyword 45,\n *  every word of a several-word query found across label, keywords and description 40 (50 when one starts the label:\n *  \"billing city\" finds Billing address › City), description 30, letters in order 15, no match 0. An empty query\n *  matches everything with 1. */\nexport function rank(query: string, label: string, keywords?: string[], description?: string): number {\n  const q = query.trim().toLowerCase();\n  if (!q) return 1;\n  const l = label.toLowerCase();\n  const at = l.indexOf(q);\n  if (at === 0) return 100 - l.length / 100;\n  if (at > 0 && /[\\s\\-/(_.]/.test(l[at - 1])) return 80;\n  if (at > 0) return 60;\n  if (keywords?.some((k) => k.toLowerCase().includes(q))) return 45;\n  const words = q.split(/\\s+/);\n  if (words.length > 1) {\n    const hay = [l, ...(keywords ?? []).map((k) => k.toLowerCase()), description?.toLowerCase() ?? \"\"];\n    if (words.every((w) => hay.some((h) => h.includes(w)))) return words.some((w) => l.startsWith(w)) ? 50 : 40;\n  }\n  if (description?.toLowerCase().includes(q)) return 30;\n  let i = 0;\n  for (const ch of l) if (ch === q[i]) i++;\n  return i === q.length ? 15 : 0;\n}\n\n/** Sorts by rank (best first, original order for ties) and drops non-matches. */\nexport function filterRanked<T>(\n  items: T[],\n  query: string,\n  get: (item: T) => { label: string; keywords?: string[]; description?: string },\n): T[] {\n  if (!query.trim()) return items;\n  return items\n    .map((item, order) => {\n      const g = get(item);\n      return { item, order, s: rank(query, g.label, g.keywords, g.description) };\n    })\n    .filter((r) => r.s > 0)\n    .sort((a, b) => b.s - a.s || a.order - b.order)\n    .map((r) => r.item);\n}\n\n/** The label with the matched run emphasised. */\nexport function Highlight({ text, query }: { text: string; query: string }) {\n  const q = query.trim().toLowerCase();\n  const at = q ? text.toLowerCase().indexOf(q) : -1;\n  if (at < 0) return <>{text}</>;\n  return (\n    <>\n      {text.slice(0, at)}\n      <mark className=\"bg-transparent font-medium text-ink\">{text.slice(at, at + q.length)}</mark>\n      {text.slice(at + q.length)}\n    </>\n  );\n}\n"},{"path":"beamline/components/_shared/messages.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy for this helper only. */\nexport const english = {\n  results: {\n    one: \"{count} result\",\n    other: \"{count} results\",\n  },\n  noMatch: \"No match for {query}\",\n  fieldOver: \"{count} over\",\n  fieldFull: \"Limit reached\",\n  fieldLeft: \"{count} left\",\n  foldMore: \"{list} and {count} more\",\n  foldShow: \"Show {count} more {noun}: {list}\",\n  statusLabel: \"Checking\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/_shared/overlay.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/overlay.module.css","content":"@layer properties, theme, base, components, utilities;\n/* the source morph's moving parts: the clip's four sides, the radius and the offset back to the trigger */\n@property --ci-t {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-r {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-b {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-l {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-x {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n@property --ci-y {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n@layer components {\n  /* Every floating layer in the system: one surface (painted by the overlay role of the look in force), one menu-item\n   look, one scrim, one enter and exit. A layer placed against a trigger opens out of it (the source morph below); one\n   without a placement arrives in --pui-motion-in scaling from its origin. Each leaves in --pui-motion-out; Radix keeps\n   it mounted until the exit animation ends. Reduced motion makes both instant (the duration tokens become 1ms). */\n  .surface {\n    z-index: var(--pui-z-popover);\n    min-width: 8rem;\n    /* never past the screen: as wide and as tall as the room beside the trigger (Radix measures it) */\n    max-width: var(--radix-popper-available-width, none);\n    max-height: var(--radix-popper-available-height, 70vh);\n    overflow: auto;\n    border-radius: var(--pui-overlay-radius, var(--pui-radius-card));\n    color: var(--pui-ink);\n    transform-origin: var(--radix-popper-transform-origin, var(--radix-dropdown-menu-content-transform-origin, center));\n    outline: none;\n  }\n  .surface[data-state=\"open\"],\n  .surface[data-state=\"delayed-open\"],\n  .surface[data-state=\"instant-open\"] {\n    animation: pop-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  .surface[data-state=\"closed\"] {\n    animation: pop-out var(--pui-motion-out) var(--pui-ease-default) forwards;\n  }\n\n  /* The source morph (research/morphing-interfaces-2026-10-04, M3): a layer placed against a trigger does not fade in\n     beside it, it opens out of it. It starts as the trigger's own box (Radix gives its size and the side and alignment\n     it was placed on), offset back onto the trigger, and grows: the side nearest the trigger is there at once, the\n     sides across follow, the far side arrives last (~200 ms), so the rows by the trigger are readable within the\n     ruled 90 ms while the outline settles. Content fades in over those 90 ms. It closes back into the trigger in the\n     ruled 180 ms. The clip ends 48px outside the box so the layer's shadow is never cut. Reduced motion: instant. */\n  .surface[data-side] {\n    --_aw: var(--radix-popper-anchor-width, 0px);\n    --_ah: var(--radix-popper-anchor-height, 0px);\n    --_gap: 6px;\n    --from-t: -48px;\n    --from-r: -48px;\n    --from-b: -48px;\n    --from-l: -48px;\n    --from-x: 0px;\n    --from-y: 0px;\n    --d-near: calc(var(--pui-duration-standard) * 0.55);\n    --d-cross: calc(var(--pui-duration-standard) * 0.85);\n    --d-far: var(--pui-duration-standard);\n    clip-path: inset(\n      var(--ci-t) var(--ci-r) var(--ci-b) var(--ci-l) round var(--pui-overlay-radius, var(--pui-radius-card))\n    );\n    translate: var(--ci-x) var(--ci-y);\n  }\n  .surface[data-side=\"bottom\"] {\n    --from-t: 0px;\n    --from-b: calc(100% - var(--_ah));\n    --from-y: calc(-1 * (var(--_ah) + var(--_gap)));\n    --d-t: var(--d-near);\n    --d-b: var(--d-far);\n    --d-l: var(--d-cross);\n    --d-r: var(--d-cross);\n  }\n  .surface[data-side=\"top\"] {\n    --from-b: 0px;\n    --from-t: calc(100% - var(--_ah));\n    --from-y: calc(var(--_ah) + var(--_gap));\n    --d-b: var(--d-near);\n    --d-t: var(--d-far);\n    --d-l: var(--d-cross);\n    --d-r: var(--d-cross);\n  }\n  .surface[data-side=\"right\"] {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n    --from-x: calc(-1 * (var(--_aw) + var(--_gap)));\n    --d-l: var(--d-near);\n    --d-r: var(--d-far);\n    --d-t: var(--d-cross);\n    --d-b: var(--d-cross);\n  }\n  .surface[data-side=\"left\"] {\n    --from-r: 0px;\n    --from-l: calc(100% - var(--_aw));\n    --from-x: calc(var(--_aw) + var(--_gap));\n    --d-r: var(--d-near);\n    --d-l: var(--d-far);\n    --d-t: var(--d-cross);\n    --d-b: var(--d-cross);\n  }\n  /* alignment along the trigger: which part of the layer the trigger sits over */\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"start\"] {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"end\"] {\n    --from-l: calc(100% - var(--_aw));\n    --from-r: 0px;\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"start\"]:dir(rtl) {\n    --from-l: calc(100% - var(--_aw));\n    --from-r: 0px;\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"end\"]:dir(rtl) {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"center\"] {\n    --from-l: calc((100% - var(--_aw)) / 2);\n    --from-r: calc((100% - var(--_aw)) / 2);\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"start\"] {\n    --from-t: 0px;\n    --from-b: calc(100% - var(--_ah));\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"end\"] {\n    --from-t: calc(100% - var(--_ah));\n    --from-b: 0px;\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"center\"] {\n    --from-t: calc((100% - var(--_ah)) / 2);\n    --from-b: calc((100% - var(--_ah)) / 2);\n  }\n  .surface[data-side]:is([data-state=\"open\"], [data-state=\"delayed-open\"], [data-state=\"instant-open\"]) {\n    animation:\n      grow-t var(--d-t) var(--pui-ease-out-strong),\n      grow-r var(--d-r) var(--pui-ease-out-strong),\n      grow-b var(--d-b) var(--pui-ease-out-strong),\n      grow-l var(--d-l) var(--pui-ease-out-strong),\n      grow-xy var(--d-cross) var(--pui-ease-out-strong),\n      settling var(--d-far) step-end;\n  }\n  .surface[data-side]:is([data-state=\"open\"], [data-state=\"delayed-open\"], [data-state=\"instant-open\"]) > * {\n    animation: content-in var(--pui-motion-in) var(--pui-ease-default) both;\n  }\n  /* one duration for every part of the exit: the layer unmounts when the first of them ends */\n  .surface[data-side][data-state=\"closed\"] {\n    animation: shrink var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  .surface[data-side][data-state=\"closed\"] > * {\n    animation: content-out var(--pui-motion-in) var(--pui-ease-default) forwards;\n  }\n  @keyframes grow-t {\n    from {\n      --ci-t: var(--from-t);\n    }\n  }\n  @keyframes grow-r {\n    from {\n      --ci-r: var(--from-r);\n    }\n  }\n  @keyframes grow-b {\n    from {\n      --ci-b: var(--from-b);\n    }\n  }\n  @keyframes grow-l {\n    from {\n      --ci-l: var(--from-l);\n    }\n  }\n  @keyframes grow-xy {\n    from {\n      --ci-x: var(--from-x);\n      --ci-y: var(--from-y);\n    }\n  }\n  /* it starts over its own trigger: until it has grown, the press that opened it must not land on a row */\n  @keyframes settling {\n    0%,\n    100% {\n      pointer-events: none;\n    }\n  }\n  @keyframes shrink {\n    to {\n      --ci-t: var(--from-t);\n      --ci-r: var(--from-r);\n      --ci-b: var(--from-b);\n      --ci-l: var(--from-l);\n      --ci-x: var(--from-x);\n      --ci-y: var(--from-y);\n      opacity: 0;\n    }\n  }\n  @keyframes content-in {\n    from {\n      opacity: 0;\n    }\n  }\n  @keyframes content-out {\n    to {\n      opacity: 0;\n    }\n  }\n  .padded {\n    padding: var(--pui-pad-popover);\n  }\n  .menu {\n    padding: var(--pui-space-1);\n  }\n  @keyframes pop-in {\n    from {\n      opacity: 0;\n      transform: scale(0.97);\n    }\n  }\n  @keyframes pop-out {\n    to {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n\n  /* rows (and the gliding plate) nest in the list: its radius minus its padding, never rounder than a chip; on the\n     row itself, so any list that uses .item gets it */\n  .item,\n  .glide {\n    --_item-radius: min(\n      var(--pui-radius-chip),\n      calc(var(--pui-overlay-radius, var(--pui-radius-card)) - var(--pui-space-1))\n    );\n  }\n\n  /* menu rows: shared by dropdown, context menu, menubar, select, combobox lists */\n  .item {\n    position: relative;\n    z-index: 1;\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-2);\n    min-height: var(--pui-control-height-sm);\n    padding: 0 var(--pui-space-2);\n    /* nested in the list: the overlay's radius minus its padding, never rounder than a chip */\n    border-radius: var(--_item-radius);\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    cursor: default;\n    user-select: none;\n    outline: none;\n  }\n  /* on a touch screen a row is a finger's target: the touch height (44px) from the tokens; a mouse keeps the dense row */\n  @media (pointer: coarse) {\n    .item {\n      min-height: var(--pui-control-height-touch);\n    }\n  }\n  /* a destructive row speaks in the danger ink only while it can be used: disabled wins (muted ink, no alarm for an\n     action that cannot happen) */\n  .item[data-tone=\"danger\"]:not([data-disabled]) {\n    color: var(--pui-red);\n  }\n  .item[data-disabled] {\n    color: var(--pui-ink-3);\n    pointer-events: none;\n  }\n  .item :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n  }\n  .item[data-tone=\"danger\"]:not([data-disabled]) :where(svg) {\n    color: currentColor;\n  }\n  .item[data-highlighted]:not([data-glide]) {\n    background: var(--pui-hover-2);\n  }\n  /* a press is acknowledged at once, over the plate, before the menu closes or the row toggles */\n  .item:active:not([data-disabled]) {\n    background: var(--pui-hover);\n  }\n  .shortcut {\n    margin-inline-start: auto;\n    padding-inline-start: var(--pui-space-4);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    letter-spacing: var(--pui-tracking-mono-caps);\n  }\n  .heading {\n    padding: var(--pui-space-1-5) var(--pui-space-2) var(--pui-space-1);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-medium);\n  }\n  .separator {\n    height: 1px;\n    margin: var(--pui-space-1) calc(var(--pui-space-1) * -1);\n    background: var(--pui-line);\n  }\n  .check {\n    margin-inline-start: auto;\n    color: var(--pui-accent) !important;\n  }\n  /* a check arrives in its row as the value changes (at once): it grows from the middle over the arrival */\n  .check :where(svg) {\n    animation: check-in var(--pui-motion-in) var(--pui-ease-out-strong);\n  }\n  @keyframes check-in {\n    from {\n      opacity: 0;\n      scale: calc(1 - 0.5 * var(--pui-motion-travel, 1));\n    }\n  }\n  /* the gliding highlight: one plate moves between rows for the pointer and the arrow keys; it appears in place */\n  .glide {\n    position: absolute;\n    z-index: 0;\n    inset-inline: var(--pui-space-1);\n    top: 0;\n    border-radius: var(--_item-radius);\n    background: var(--pui-hover-2);\n    pointer-events: none;\n    opacity: 0;\n    transition: opacity var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .glide[data-on] {\n    opacity: 1;\n    transition:\n      transform var(--pui-duration-fast) var(--pui-ease-standard),\n      height var(--pui-duration-fast) var(--pui-ease-standard),\n      opacity 0ms;\n  }\n  .glide[data-jump] {\n    transition: opacity 0ms;\n  }\n  /* a plate over a row or grid of cells takes each cell's size and travels both ways */\n  .glide[data-axis=\"xy\"] {\n    inset-inline: auto;\n    left: 0;\n  }\n  .glide[data-axis=\"xy\"][data-on] {\n    transition:\n      transform var(--pui-duration-fast) var(--pui-ease-standard),\n      width var(--pui-duration-fast) var(--pui-ease-standard),\n      height var(--pui-duration-fast) var(--pui-ease-standard),\n      opacity 0ms;\n  }\n\n  /* a layer that has begun to close is out of the way at once: for the length of its exit it takes no pointer, so a tap\n     meant for the page behind (the next row, the button the panel covered) lands there, never on the leaving ghost.\n     Focus has already gone back (Radix) and it unmounts when the exit ends. */\n  .scrim[data-state=\"closed\"],\n  .surface[data-state=\"closed\"],\n  .dialog[data-state=\"closed\"],\n  .sheet[data-state=\"closed\"] {\n    pointer-events: none;\n  }\n\n  /* modal layers */\n  .scrim {\n    position: fixed;\n    inset: 0;\n    z-index: var(--pui-z-overlay);\n    background: var(--pui-scrim);\n    backdrop-filter: blur(var(--pui-blur-overlay));\n  }\n  .scrim[data-state=\"open\"] {\n    animation: fade-in var(--pui-fade-in) var(--pui-ease-default);\n  }\n  .scrim[data-state=\"closed\"] {\n    animation: fade-out var(--pui-fade-out) var(--pui-ease-default) forwards;\n  }\n  @keyframes fade-in {\n    from {\n      opacity: 0;\n    }\n  }\n  @keyframes fade-out {\n    to {\n      opacity: 0;\n    }\n  }\n  .dialog {\n    /* the content answers to the dialog's own width (a footer's buttons share the row on a phone) */\n    container-type: inline-size;\n    position: fixed;\n    z-index: var(--pui-z-modal);\n    left: 50%;\n    top: 50%;\n    width: min(calc(100vw - var(--pui-space-8)), var(--pui-dialog-width, var(--_dialog-width, 440px)));\n    max-height: calc(100dvh - var(--pui-space-12));\n    overflow: hidden;\n    translate: -50% -50%;\n    border-radius: var(--pui-dialog-radius, var(--pui-radius-window));\n    color: var(--pui-ink);\n    outline: none;\n    /* a size chosen while open (a step that needs more room) eases the width in place, centred */\n    transition: width var(--pui-duration-standard) var(--pui-ease-standard);\n  }\n  .dialog[data-state=\"open\"] {\n    animation: dialog-in var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .dialog[data-state=\"closed\"] {\n    animation: dialog-out var(--pui-motion-out) var(--pui-ease-default) forwards;\n  }\n  /* A dialog opened from something (use-open-from.ts) grows out of it: it starts as the opener's box, offset back onto\n     it, and grows to its own place over the considered duration while its content fades in; it closes back into the\n     opener in the ruled 180 ms. Shares the source morph's moving parts and guard with .surface. */\n  .dialog[data-open-from] {\n    clip-path: inset(\n      var(--ci-t) var(--ci-r) var(--ci-b) var(--ci-l) round var(--pui-dialog-radius, var(--pui-radius-window))\n    );\n    transform: translate(var(--ci-x), var(--ci-y));\n  }\n  .dialog[data-open-from][data-state=\"open\"] {\n    animation:\n      grow-t var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-r var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-b var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-l var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-xy calc(var(--pui-duration-considered) * 0.9) var(--pui-ease-out-strong),\n      settling var(--pui-duration-considered) step-end;\n  }\n  .dialog[data-open-from][data-state=\"open\"] > * {\n    animation: content-in var(--pui-duration-standard) var(--pui-ease-default) calc(var(--pui-duration-standard) * 0.35)\n      both;\n  }\n  .dialog[data-open-from][data-state=\"closed\"] {\n    animation: shrink var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  .dialog[data-open-from][data-state=\"closed\"] > * {\n    animation: content-out var(--pui-motion-in) var(--pui-ease-default) forwards;\n  }\n  /* A sheet: a panel from an edge. It slides in from its edge over the considered duration on the drawer curve and\n     goes back out to that edge in the ruled departure (a layer closes back into where it came from). The edge is\n     logical: an end (\"right\") sheet comes from the left in a right-to-left page. Its content answers to its width. */\n  .sheet {\n    --_sx: 0px;\n    --_sy: 0px;\n    container-type: inline-size;\n    /* where a finger has swiped it toward its edge (sheet.tsx); it settles back over the departure */\n    translate: var(--_swipe, 0px) 0;\n  }\n  /* a side panel leaves sideways pans to the swipe and keeps vertical scrolling native, in its body too */\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]),\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]) [data-slot=\"sheet-body\"] {\n    touch-action: pan-y;\n  }\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]) {\n    transition:\n      var(--m-transition),\n      translate var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .sheet[data-swiping] {\n    transition: none;\n  }\n  .sheet[data-side=\"right\"] {\n    --_sx: 100%;\n  }\n  .sheet[data-side=\"left\"] {\n    --_sx: -100%;\n  }\n  .sheet[data-side=\"right\"]:dir(rtl) {\n    --_sx: -100%;\n  }\n  .sheet[data-side=\"left\"]:dir(rtl) {\n    --_sx: 100%;\n  }\n  .sheet[data-side=\"top\"] {\n    --_sy: -100%;\n  }\n  .sheet[data-side=\"bottom\"] {\n    --_sy: 100%;\n  }\n  .sheet[data-state=\"open\"] {\n    animation: sheet-in var(--pui-duration-considered) var(--pui-ease-drawer) both;\n  }\n  .sheet[data-state=\"closed\"] {\n    animation: sheet-out var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  @keyframes sheet-in {\n    from {\n      translate: var(--_sx) var(--_sy);\n    }\n  }\n  @keyframes sheet-out {\n    to {\n      translate: var(--_sx) var(--_sy);\n    }\n  }\n  @keyframes dialog-in {\n    from {\n      opacity: 0;\n      transform: scale(0.96);\n    }\n  }\n  @keyframes dialog-out {\n    to {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n}\n\n/* Reduced motion: surfaces and dialogs fade without scaling or growing, over the fade tokens (restrained under reduced,\n   1ms under none; APPEARANCE A06). */\n@media (prefers-reduced-motion: reduce) {\n  .surface,\n  .dialog,\n  .sheet {\n    animation-name: fade-in !important;\n    animation-duration: var(--pui-fade-in) !important;\n  }\n  .surface[data-state=\"closed\"],\n  .dialog[data-state=\"closed\"],\n  .sheet[data-state=\"closed\"] {\n    animation-name: fade-out !important;\n    animation-duration: var(--pui-fade-out) !important;\n  }\n}\n@layer components {\n  :global(.pui[data-motion=\"reduced\"]) .surface,\n  :global(.pui[data-motion=\"none\"]) .surface,\n  :global(.pui[data-motion=\"reduced\"]) .dialog,\n  :global(.pui[data-motion=\"none\"]) .dialog,\n  :global(.pui[data-motion=\"reduced\"]) .sheet,\n  :global(.pui[data-motion=\"none\"]) .sheet {\n    animation-name: fade-in;\n    animation-duration: var(--pui-fade-in);\n  }\n  :global(.pui[data-motion=\"reduced\"]) .surface[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .surface[data-state=\"closed\"],\n  :global(.pui[data-motion=\"reduced\"]) .dialog[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .dialog[data-state=\"closed\"],\n  :global(.pui[data-motion=\"reduced\"]) .sheet[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .sheet[data-state=\"closed\"] {\n    animation-name: fade-out;\n    animation-duration: var(--pui-fade-out);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.surface {\n  composes: m-overlay from global;\n}\n.dialog {\n  composes: m-overlay from global;\n}\n"},{"path":"beamline/components/_shared/popover-search.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/popover-search.tsx","content":"\"use client\";\n// organism-allow-file: raw-control -- the shared part every element draws this control from\n\nimport type { InputHTMLAttributes, Ref } from \"react\";\nimport { Search } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface PopoverSearchProps extends InputHTMLAttributes<HTMLInputElement> {\n  /** The search's name (the placeholder is a hint, not a label). */\n  \"aria-label\": string;\n  /** The row around the field (the input itself takes className). */\n  rowClassName?: string;\n  ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * The search row at the top of a dropdown list (multi-select, country, voice, tree): a magnifier and a bare field over a\n * hairline. The field keeps focus while the arrows move through the list, so the caller passes the combobox wiring\n * (role, aria-controls, aria-activedescendant, onKeyDown) through.\n */\nexport function PopoverSearch({ rowClassName, className, ref, ...props }: PopoverSearchProps) {\n  return (\n    <div className={cn(\"flex h-10 flex-none items-center gap-2 border-b border-line-soft px-3\", rowClassName)}>\n      <Search aria-hidden className=\"size-4 shrink-0 text-ink-3\" />\n      <input\n        ref={ref}\n        autoComplete=\"off\"\n        spellCheck={false}\n        {...props}\n        className={cn(\n          \"h-full min-w-0 flex-1 bg-transparent text-sm text-ink outline-none placeholder:text-ink-3\",\n          className,\n        )}\n      />\n    </div>\n  );\n}\n"},{"path":"beamline/components/_shared/roll.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/roll.ts","content":"/**\n * Rolls the text of a field or any text part from one value to the next (number field steps, a time segment): the old text leaves up and the new one\n * arrives from below for an increase, the other way for a decrease. The field's value changes at once; a still copy of\n * each text is drawn over the field for the roll and removed after, so typing and the caret are never touched.\n */\nconst running = new WeakMap<HTMLElement, () => void>();\n\nexport function rollText(\n  input: HTMLElement | null,\n  from: string,\n  to: string,\n  direction: 1 | -1,\n  { duration, easing }: { duration: number; easing: string },\n) {\n  if (!input || from === to || duration < 2 || !input.offsetParent) return;\n  running.get(input)?.();\n  const host = input.offsetParent as HTMLElement;\n  const cs = getComputedStyle(input);\n  const box = document.createElement(\"span\");\n  box.setAttribute(\"aria-hidden\", \"true\");\n  Object.assign(box.style, {\n    position: \"absolute\",\n    left: `${input.offsetLeft}px`,\n    top: `${input.offsetTop}px`,\n    width: `${input.offsetWidth}px`,\n    height: `${input.offsetHeight}px`,\n    overflow: \"hidden\",\n    pointerEvents: \"none\",\n  });\n  const line = (text: string) => {\n    const s = document.createElement(\"span\");\n    s.textContent = text;\n    Object.assign(s.style, {\n      position: \"absolute\",\n      inset: \"0\",\n      display: \"flex\",\n      alignItems: \"center\",\n      justifyContent:\n        cs.textAlign === \"right\" || cs.textAlign === \"end\"\n          ? \"flex-end\"\n          : cs.textAlign === \"center\"\n            ? \"center\"\n            : \"flex-start\",\n      paddingInline: `${cs.paddingLeft} ${cs.paddingRight}`,\n      font: cs.font,\n      fontVariantNumeric: cs.fontVariantNumeric,\n      letterSpacing: cs.letterSpacing,\n      color: cs.color,\n      whiteSpace: \"pre\",\n    });\n    return s;\n  };\n  const old = line(from),\n    next = line(to);\n  box.append(old, next);\n  host.appendChild(box);\n  const color = input.style.color;\n  input.style.color = \"transparent\";\n  const shift = 0.7 * direction;\n  const a = old.animate(\n    [\n      { transform: \"none\", opacity: 1 },\n      { transform: `translateY(${-shift * 100}%)`, opacity: 0 },\n    ],\n    { duration, easing },\n  );\n  next.animate(\n    [\n      { transform: `translateY(${shift * 100}%)`, opacity: 0 },\n      { transform: \"none\", opacity: 1 },\n    ],\n    { duration, easing },\n  );\n  const done = () => {\n    running.delete(input);\n    box.remove();\n    input.style.color = color;\n  };\n  running.set(input, done);\n  a.finished.then(done, done);\n}\n"},{"path":"beamline/components/_shared/text-action.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/text-action.module.css","content":"@layer properties, theme, base, components, utilities;\n/* An action that reads as words in the flow of a form or a sentence: \"Use another account\", \"Resend code\", \"Select\n   all\", \"Show fewer\". One recipe so every one of them answers alike; a framed action is a Button. */\n@layer components {\n  .root {\n    border-radius: var(--pui-radius-chip);\n    text-underline-offset: 0.25em;\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root:hover {\n    transition-duration: 0ms;\n  }\n  .root:disabled {\n    pointer-events: none;\n    opacity: var(--pui-opacity-disabled);\n  }\n  /* accent: the way forward out of the form (a link's colour, underlined on hover) */\n  .root[data-tone=\"accent\"] {\n    color: var(--pui-accent-ink);\n    font-weight: var(--pui-weight-medium);\n  }\n  .root[data-tone=\"accent\"]:hover {\n    text-decoration-line: underline;\n  }\n  /* quiet: a side action in the caller's ink that takes full ink on hover */\n  .root[data-tone=\"quiet\"]:hover:not(:disabled) {\n    color: var(--pui-ink);\n  }\n  /* inline: inside a sentence, underlined at rest so it reads as a link without colour */\n  .root[data-tone=\"inline\"] {\n    text-decoration-line: underline;\n  }\n}\n"},{"path":"beamline/components/_shared/text-action.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/text-action.tsx","content":"\"use client\";\n// organism-allow-file: raw-control -- the shared part every element draws this control from\n\nimport type { ButtonHTMLAttributes, Ref } from \"react\";\nimport * as Slot from \"@radix-ui/react-slot\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./text-action.module.css\";\n\nexport interface TextActionProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  /** accent: the way forward (a link's colour, underlined on hover); quiet: a side action in the surrounding ink that\n   *  takes full ink on hover; inline: inside a sentence, underlined at rest. */\n  tone?: \"accent\" | \"quiet\" | \"inline\";\n  /** Render the single child (a link) with the action's look instead of a <button>. */\n  asChild?: boolean;\n  ref?: Ref<HTMLButtonElement>;\n}\n\n/** An action that reads as words, not a framed control: \"Resend code\", \"Select all\", \"Show fewer\". */\nexport function TextAction({ tone = \"accent\", asChild, type = \"button\", className, ref, ...props }: TextActionProps) {\n  const shared = { \"data-tone\": tone, className: cn(styles.root, className) };\n  if (asChild) return <Slot.Root ref={ref} {...shared} {...props} />;\n  return <button ref={ref} type={type} {...shared} {...props} />;\n}\n"},{"path":"beamline/components/label/label.tsx","type":"registry:file","target":"src/ui/beamline/components/label/label.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport type { ComponentProps } from \"react\";\nimport * as RadixLabel from \"@radix-ui/react-label\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface LabelProps extends ComponentProps<typeof RadixLabel.Root> {\n  /** Shows the marker and announces \"required\". */\n  required?: boolean;\n  /** The control says \"required\" itself (a native required input, a radiogroup with aria-required): the label shows\n   * the marker only, so the control's name stays the label's words and the state is announced once. */\n  requiredOnControl?: boolean;\n}\n\n/** The text label tied to a form control. It dims with a disabled control next to it (peer) or inside it (group). */\nexport function Label({ required = false, requiredOnControl = false, className, children, ...props }: LabelProps) {\n  const i18n = useMessages(\"label\", englishMessages);\n\n  return (\n    <RadixLabel.Root\n      data-slot=\"label\"\n      {...props}\n      className={cn(\n        \"inline-flex items-center gap-[var(--pui-space-1)] text-sm leading-tight font-medium text-ink select-none group-data-[disabled]:opacity-[var(--pui-opacity-disabled)] peer-disabled:opacity-[var(--pui-opacity-disabled)]\",\n        className,\n      )}\n    >\n      {children}\n      {required && (\n        <>\n          <span aria-hidden className=\"text-red\">\n            *\n          </span>\n          {!requiredOnControl && <span className=\"sr-only\">{i18n(\"required\")}</span>}\n        </>\n      )}\n    </RadixLabel.Root>\n  );\n}\n\nexport default Label;\n"},{"path":"beamline/components/label/messages.ts","type":"registry:file","target":"src/ui/beamline/components/label/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  required: \"required\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/multi-select/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/multi-select/defaults.ts","content":"/** Pure allowed presentation defaults for MultiSelect; no component or runtime imports. */\nexport interface MultiSelectDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/multi-select/messages.ts","type":"registry:file","target":"src/ui/beamline/components/multi-select/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  placeholder: \"Choose\",\n  remove: \"Remove {label}\",\n  search: \"Search\",\n  searchOptions: \"Search options\",\n  options: \"Options\",\n  ofChosen: \"{length} of {length2} chosen\",\n  selectMatches: {\n    one: \"Select {count} match\",\n    other: \"Select {count} matches\",\n  },\n  selectAll: \"Select all\",\n  clear: \"Clear\",\n  noneChosen: \"none chosen\",\n  moved: \"{label} moved to {position} of {count}\",\n  reorderHint: \"Drag a chip, or Alt+← and Alt+→, to change the order.\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/multi-select/multi-select.tsx","type":"registry:file","target":"src/ui/beamline/components/multi-select/multi-select.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 { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { MultiSelectDefaults } from \"./defaults\";\nexport type { MultiSelectDefaults } from \"./defaults\";\n\nimport {\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type KeyboardEvent,\n  type CSSProperties,\n  type Ref,\n  type ReactNode,\n} from \"react\";\nimport { useDrag } from \"../../hooks/use-drag\";\nimport { ChevronsUpDown, Lock } from \"lucide-react\";\nimport * as RadixPopover from \"@radix-ui/react-popover\";\nimport { Field, FieldFrame, useFieldIds, type FieldSize } from \"../_shared/field\";\nimport type { FieldClassNames } from \"../_shared/field\";\nimport { FieldChip } from \"../_shared/chip\";\nimport { FoldMore } from \"../_shared/fold-more\";\nimport { Listbox, useListbox, type ListOption } from \"../_shared/listbox\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useFlip } from \"../../hooks/use-flip\";\nimport { useFit } from \"../../hooks/use-fit\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { PopoverSearch } from \"../_shared/popover-search\";\nimport { TextAction } from \"../_shared/text-action\";\nimport overlay from \"../_shared/overlay.module.css\";\n\nexport type MultiSelectOption = ListOption;\n\nexport interface MultiSelectProps {\n  /** Chosen values that can't be taken out, each with its reason (\"The creator's company always watches\"): the chip\n   *  shows a lock instead of ×, says the reason to screen readers and on hover, and its row and Clear leave it. */\n  locked?: Record<string, string>;\n  /** Visible label or accessible name for the control. Required. */\n  label: ReactNode;\n  /** Shows the label visually while keeping its accessible name; defaults to true. Takes precedence over hideLabel. */\n  showLabel?: boolean;\n  /** Hides the label; defaults to false. @deprecated Use showLabel={false} instead. */\n  hideLabel?: boolean;\n  /** Available choices with their labels and values. Required. */\n  options: MultiSelectOption[];\n  /** Controlled selected option values; omit to use defaultValue. */\n  value?: string[];\n  /** Initial selected option values when uncontrolled. Defaults to an empty list. */\n  defaultValue?: string[];\n  /** Reports selected option values after an edit; no callback by default. */\n  onValueChange?: (value: string[]) => void;\n  /** Text shown before a value is entered or selected. Defaults to \"Choose\". */\n  placeholder?: string;\n  /** Supporting text beneath the label or heading. Defaults to no supporting text. */\n  description?: ReactNode;\n  /** Error message shown in place of the usual supporting text. Defaults to no error message. */\n  error?: ReactNode;\n  /** At most this many chips in the field before \"+N\". By default as many as the field's width holds on one line: the\n   *  rest fold into \"+N\" and come back as the field grows. */\n  maxVisible?: number;\n  /** The order of the chosen values matters (a ranking, the order columns appear in): chips follow `value`'s order and\n   *  can be dragged to a new place, or moved with Alt+← and Alt+→ from a chip's remove button; `onValueChange` carries\n   *  the new order and the move is announced. Off by default: chips follow the options' order, as before. */\n  sortable?: boolean;\n  /** Show a search field above the list. Default when there are more than 7 options. */\n  searchable?: boolean;\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: FieldSize;\n  /** Prevents interaction with the control. Defaults to false. */\n  disabled?: boolean;\n  /** DOM id used to connect the control and its label. Defaults to an automatically generated control id. */\n  id?: string;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: Pick<FieldClassNames, \"root\" | \"label\" | \"description\" | \"error\"> &\n    Partial<\n      Record<\n        \"frame\" | \"trailing\" | \"values\" | \"trigger\" | \"content\" | \"search\" | \"list\" | \"footer\" | \"selectAll\" | \"clear\",\n        string\n      >\n    >;\n  /** Receives the button or trigger; no ref by default. */\n  ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * Several values from a known list, shown as removable chips in the field. The list opens with a search field and\n * checkable rows: ↑ ↓ move, Enter or Space toggles, Escape closes.\n */\nexport function MultiSelect({\n  label,\n  hideLabel: hideLabelProp,\n  showLabel,\n  options,\n  value: valueProp,\n  defaultValue = [],\n  onValueChange,\n  placeholder: placeholderProp,\n  description,\n  error,\n  maxVisible,\n  sortable = false,\n  locked,\n  searchable,\n  size: sizeProp,\n  disabled,\n  id,\n  className,\n  classNames,\n  style,\n  ref,\n}: MultiSelectProps) {\n  const i18n = useMessages(\"multi-select\", englishMessages);\n  const placeholder = placeholderProp === undefined ? i18n(\"placeholder\") : placeholderProp;\n\n  useDeprecatedProp(\"MultiSelect\", \"hideLabel\", \"showLabel={false}\", hideLabelProp !== undefined);\n  const hideLabel = showLabel === undefined ? hideLabelProp : !showLabel;\n  const scopeDefaults = useComponentDefaults<MultiSelectDefaults>(\"multi-select\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const [value, set] = useControllable(valueProp, defaultValue, onValueChange);\n  const isLocked = (v: string) => !!locked?.[v] && value.includes(v);\n  const toggle = (v: string) => {\n    if (isLocked(v)) return;\n    set(value.includes(v) ? value.filter((x) => x !== v) : [...value, v]);\n  };\n  // a locked choice's row stays checked and can't be unticked; its reason is its description\n  const listOptions = useMemo(\n    () =>\n      locked\n        ? options.map((o) =>\n            locked[o.value] && value.includes(o.value) ? { ...o, disabled: true, description: locked[o.value] } : o,\n          )\n        : options,\n    [options, locked, value],\n  );\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const { filtered, active, setActive, move, viaKeys, current } = useListbox(listOptions, query);\n  // a sortable field says how to reorder unless the app says something itself\n  const help = description ?? (sortable ? i18n(\"reorderHint\") : undefined);\n  const { controlId, descriptionId, errorId, aria } = useFieldIds(id, { description: help, error });\n  const listId = useId();\n  const trigger = useRef<HTMLButtonElement>(null);\n  const values = useRef<HTMLSpanElement>(null);\n  useFlip(values);\n  const attach = useComposedRef(trigger, ref);\n  const container = usePortalContainer();\n  // a drag in progress holds its own order until it is let go (Escape or a lost pointer puts it back)\n  const [dragOrder, setDragOrder] = useState<string[] | null>(null);\n  const [moved, setMoved] = useState(\"\");\n  const order = dragOrder ?? value;\n  const chosen = sortable\n    ? (order.map((v) => options.find((o) => o.value === v)).filter(Boolean) as ListOption[])\n    : options.filter((o) => value.includes(o.value));\n  const announce = (v: string, list: string[]) => {\n    const o = options.find((x) => x.value === v);\n    if (o) setMoved(i18n(\"moved\", { label: o.label, position: list.indexOf(v) + 1, count: list.length }));\n  };\n  const shift = (v: string, delta: -1 | 1) => {\n    const i = value.indexOf(v);\n    const j = i + delta;\n    if (i < 0 || j < 0 || j >= value.length) return;\n    const next = [...value];\n    [next[i], next[j]] = [next[j], next[i]];\n    set(next);\n    announce(v, next);\n  };\n  const withSearch = searchable ?? options.length > 7;\n  // the field's width decides how many chips show (one line); the rest fold into \"+N\", in order, and come back as the\n  // field grows (the same fold as chip-group, @/hooks/use-fit)\n  const chipSet = chosen.map((o) => `${o.value}\\u0000${o.label}`).join(\"\\u0001\");\n  const fits = useFit(values, { items: chosen.length, lines: 1 }, chipSet);\n  const limit = Math.min(maxVisible ?? chosen.length, fits);\n  const folded = chosen.slice(limit);\n  // the footer can take every option at once, or every match while searching (disabled ones never)\n  const pickable = (query.trim() ? filtered : options).filter((o) => !o.disabled);\n  const allOn = pickable.every((o) => value.includes(o.value));\n\n  const keys = (e: KeyboardEvent) => {\n    if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n      e.preventDefault();\n      move(e.key === \"ArrowDown\" ? 1 : -1);\n    } else if (e.key === \"Enter\" || (e.key === \" \" && !withSearch)) {\n      e.preventDefault();\n      if (current && !current.disabled) toggle(current.value);\n    }\n  };\n  return (\n    <Field\n      label={label}\n      hideLabel={hideLabel}\n      description={help}\n      error={error}\n      controlId={controlId}\n      descriptionId={descriptionId}\n      errorId={errorId}\n      className={className}\n      classNames={classNames}\n      style={style}\n    >\n      <RadixPopover.Root\n        open={open && !disabled}\n        onOpenChange={(o) => {\n          setOpen(o);\n          if (!o) setQuery(\"\");\n        }}\n      >\n        <RadixPopover.Anchor asChild>\n          <div>\n            <FieldFrame\n              classNames={classNames}\n              size={size}\n              invalid={!!error}\n              disabled={disabled}\n              className=\"h-auto cursor-pointer py-[var(--pui-size-1,0.25rem)] [--pui-nest-inset:calc((var(--_field-height)-1.5rem)/2)] [--pui-nest-radius:var(--pui-field-radius,var(--pui-radius-control))]\"\n              onPointerDown={(e) => {\n                // the frame's padding, the empty part of the chip row and the chevron open the list, like the trigger\n                const t = e.target as HTMLElement;\n                if (\n                  t === e.currentTarget ||\n                  t.dataset.slot === \"multi-select-values\" ||\n                  t.closest(\"[data-adornment]\") ||\n                  t.closest(\"[data-fit-more]\")\n                ) {\n                  e.preventDefault();\n                  setOpen(true);\n                }\n              }}\n              trailing={<ChevronsUpDown className=\"size-4\" />}\n            >\n              <span\n                ref={values}\n                data-slot=\"multi-select-values\"\n                className={cn(\"flex min-w-0 flex-1 flex-wrap items-center gap-1\", classNames?.values)}\n              >\n                {chosen.slice(0, limit).map((o) =>\n                  sortable && !disabled ? (\n                    <SortableChip\n                      key={o.value}\n                      value={o.value}\n                      values={values}\n                      order={order}\n                      onDragOrder={setDragOrder}\n                      onDrop={(next) => {\n                        setDragOrder(null);\n                        if (next.join(\"\\u0001\") !== value.join(\"\\u0001\")) {\n                          set(next);\n                          announce(o.value, next);\n                        }\n                      }}\n                      onShift={(d) => shift(o.value, d)}\n                      dragging={dragOrder !== null}\n                    >\n                      <FieldChip\n                        removeLabel={i18n(\"remove\", { label: o.label })}\n                        onRemove={isLocked(o.value) ? undefined : () => toggle(o.value)}\n                      >\n                        <ChipWords label={o.label} reason={isLocked(o.value) ? locked![o.value] : undefined} />\n                      </FieldChip>\n                    </SortableChip>\n                  ) : (\n                    <span key={o.value} data-fit-item data-flip={o.value} className=\"inline-flex min-w-0\">\n                      <FieldChip\n                        removeLabel={i18n(\"remove\", { label: o.label })}\n                        disabled={disabled}\n                        onRemove={isLocked(o.value) ? undefined : () => toggle(o.value)}\n                      >\n                        <ChipWords label={o.label} reason={isLocked(o.value) ? locked![o.value] : undefined} />\n                      </FieldChip>\n                    </span>\n                  ),\n                )}\n                {sortable && (\n                  <span className=\"sr-only\" role=\"status\" data-slot=\"multi-select-moved\">\n                    {moved}\n                  </span>\n                )}\n                {folded.length > 0 && (\n                  // the folded chips by name on hover; the trigger's own name already lists every choice\n                  <FoldMore names={folded.map((o) => o.label)} hidden className=\"cursor-pointer\" />\n                )}\n                <RadixPopover.Trigger asChild>\n                  <button // organism-allow: raw-control -- the field's own trigger, sized to what the chips leave\n                    ref={attach}\n                    type=\"button\"\n                    data-slot=\"multi-select\"\n                    {...aria}\n                    aria-haspopup=\"listbox\"\n                    aria-expanded={open}\n                    disabled={disabled}\n                    aria-label={`${typeof label === \"string\" ? label : i18n(\"options\")}: ${chosen.length ? chosen.map((o) => o.label).join(\", \") : i18n(\"noneChosen\")}`}\n                    className={cn(\n                      \"h-[calc(var(--_field-height)-8px)] flex-1 text-start text-ink-3 outline-none\",\n                      // with chips the trigger only fills what is left of the row, so it never opens an empty second row\n                      // its negative start margin cancels the row's gap: when the chips that fit fill the line to\n                      // the pixel, the (empty) trigger still shares it instead of opening a second line\n                      chosen.length ? \"-ms-1 min-w-0\" : \"min-w-[4ch]\",\n                      classNames?.trigger,\n                    )}\n                  >\n                    {chosen.length ? \"\" : placeholder}\n                  </button>\n                </RadixPopover.Trigger>\n              </span>\n            </FieldFrame>\n          </div>\n        </RadixPopover.Anchor>\n        {container !== null && (\n          <RadixPopover.Portal container={container}>\n            <RadixPopover.Content\n              data-slot=\"multi-select-list\"\n              sideOffset={6}\n              align=\"start\"\n              collisionPadding={8}\n              onKeyDown={withSearch ? undefined : keys}\n              onOpenAutoFocus={(e) => {\n                if (!withSearch) {\n                  e.preventDefault();\n                  (e.currentTarget as HTMLElement).focus();\n                }\n              }}\n              className={cn(\n                overlay.surface,\n                \"w-[var(--radix-popover-trigger-width)] min-w-[240px] outline-none\",\n                classNames?.content,\n              )}\n              tabIndex={-1}\n              aria-activedescendant={!withSearch && current ? `${listId}-${active}` : undefined}\n            >\n              {withSearch && (\n                <PopoverSearch\n                  data-slot=\"multi-select-search\"\n                  autoFocus\n                  value={query}\n                  onChange={(e) => setQuery(e.target.value)}\n                  onKeyDown={keys}\n                  placeholder={i18n(\"search\")}\n                  aria-label={i18n(\"searchOptions\")}\n                  role=\"combobox\"\n                  aria-expanded\n                  aria-controls={listId}\n                  aria-activedescendant={current ? `${listId}-${active}` : undefined}\n                  className={classNames?.search}\n                />\n              )}\n              <Listbox\n                className={classNames?.list}\n                id={listId}\n                label={typeof label === \"string\" ? label : i18n(\"options\")}\n                options={filtered}\n                query={query}\n                active={active}\n                onActive={setActive}\n                viaKeys={viaKeys}\n                isSelected={(v) => value.includes(v)}\n                onPick={(o) => toggle(o.value)}\n                multiple\n              />\n              <div\n                data-slot=\"multi-select-footer\"\n                className={cn(\n                  \"flex items-center justify-between border-t border-line-soft px-3 py-1.5 text-xs text-ink-3\",\n                  classNames?.footer,\n                )}\n              >\n                <span aria-live=\"polite\">{i18n(\"ofChosen\", { length: value.length, length2: options.length })}</span>\n                <span className=\"flex items-center gap-1\">\n                  <TextAction\n                    tone=\"quiet\"\n                    disabled={!pickable.length || allOn}\n                    onClick={() => set([...value, ...pickable.map((o) => o.value).filter((v) => !value.includes(v))])}\n                    data-slot=\"multi-select-all\"\n                    className={cn(\"px-1\", classNames?.selectAll)}\n                  >\n                    {query.trim() ? i18n(\"selectMatches\", { count: pickable.length }) : i18n(\"selectAll\")}\n                  </TextAction>\n                  <TextAction\n                    tone=\"quiet\"\n                    disabled={!value.some((v) => !isLocked(v))}\n                    onClick={() => set(value.filter(isLocked))}\n                    data-slot=\"multi-select-clear\"\n                    className={cn(\"px-1\", classNames?.clear)}\n                  >\n                    {i18n(\"clear\")}\n                  </TextAction>\n                </span>\n              </div>\n            </RadixPopover.Content>\n          </RadixPopover.Portal>\n        )}\n      </RadixPopover.Root>\n    </Field>\n  );\n}\n\n/** A chip that moves: dragged across the row (its place follows the pointer, the others slide aside) or moved with\n *  Alt+← / Alt+→ while its remove button has focus. A press on the remove button stays a press. */\nfunction SortableChip({\n  value,\n  values,\n  order,\n  onDragOrder,\n  onDrop,\n  onShift,\n  dragging,\n  children,\n}: {\n  value: string;\n  values: { current: HTMLSpanElement | null };\n  order: string[];\n  onDragOrder: (order: string[] | null) => void;\n  onDrop: (order: string[]) => void;\n  onShift: (delta: -1 | 1) => void;\n  dragging: boolean;\n  children: ReactNode;\n}) {\n  const host = useRef<HTMLSpanElement>(null);\n  const latest = useRef(order);\n  if (!dragging) latest.current = order;\n  const place = (x: number, y: number) => {\n    // the chip whose box holds the pointer, or the nearest one on the pointer's line, takes this chip's place\n    const row = values.current;\n    if (!row) return;\n    const chips = [...row.querySelectorAll<HTMLElement>(\"[data-sortable-chip]\")].filter(\n      (c) => c.dataset.sortableChip !== value,\n    );\n    const rtl = getComputedStyle(row).direction === \"rtl\";\n    let index = 0;\n    for (const c of chips) {\n      // where the chip rests, not where its slide has it this frame (useFlip moves it with a transform)\n      const r = c.getBoundingClientRect();\n      const m = new DOMMatrixReadOnly(\n        getComputedStyle(c).transform === \"none\" ? undefined : getComputedStyle(c).transform,\n      );\n      const left = r.left - m.m41;\n      const top = r.top - m.m42;\n      const mid = left + r.width / 2;\n      const before = y > top + r.height || (y >= top && (rtl ? x < mid : x > mid));\n      if (before) index++;\n    }\n    const rest = latest.current.filter((v) => v !== value);\n    const visible = chips.map((c) => c.dataset.sortableChip!);\n    // folded chips keep their places after the visible ones\n    const target = index < visible.length ? rest.indexOf(visible[index]) : rest.length;\n    const next = [...rest.slice(0, target), value, ...rest.slice(target)];\n    if (next.join(\"\\u0001\") === latest.current.join(\"\\u0001\")) return;\n    // held at once too: a release can come before the next render\n    latest.current = next;\n    onDragOrder(next);\n  };\n  const { dragging: mine } = useDrag(\n    host,\n    {\n      onPress: (_p, e) => !(e.target as HTMLElement).closest(\"button\"),\n      onStart: (p) => {\n        onDragOrder([...latest.current]);\n        place(p.x, p.y);\n      },\n      onMove: (p) => place(p.x, p.y),\n      onEnd: () => onDrop(latest.current),\n      onCancel: () => onDragOrder(null),\n    },\n    { axis: \"both\" },\n  );\n  return (\n    <span\n      ref={host}\n      data-fit-item\n      data-flip={value}\n      data-sortable-chip={value}\n      data-dragging={mine ? \"\" : undefined}\n      className={cn(\n        \"inline-flex min-w-0 cursor-grab touch-none rounded-[var(--pui-radius-chip)]\",\n        mine && \"z-10 cursor-grabbing shadow-[0_0_0_2px_var(--pui-accent)]\",\n        dragging && !mine && \"transition-none\",\n      )}\n      onKeyDown={(e) => {\n        if (!e.altKey || (e.key !== \"ArrowLeft\" && e.key !== \"ArrowRight\")) return;\n        e.preventDefault();\n        e.stopPropagation();\n        const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\";\n        onShift((e.key === \"ArrowRight\") !== rtl ? 1 : -1);\n        // keep focus on the moved chip's remove button after the row re-orders\n        const button = e.target as HTMLElement;\n        requestAnimationFrame(() => button.focus());\n      }}\n    >\n      {children}\n    </span>\n  );\n}\n\nexport default MultiSelect;\n\n/** A chip's words; a locked one carries a lock and its reason (said to screen readers, shown on hover). */\nfunction ChipWords({ label, reason }: { label: string; reason?: string }) {\n  if (!reason) return <>{label}</>;\n  return (\n    <span className=\"inline-flex min-w-0 items-center gap-1\" title={reason} data-slot=\"multi-select-locked\">\n      <Lock aria-hidden className=\"size-3 shrink-0 text-ink-3\" />\n      <span className=\"truncate\">{label}</span>\n      <span className=\"sr-only\">, {reason}</span>\n    </span>\n  );\n}\n"},{"path":"beamline/components/spinner/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/spinner/defaults.ts","content":"/** Pure allowed presentation defaults for Spinner; no component or runtime imports. */\nexport interface SpinnerDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/spinner/messages.ts","type":"registry:file","target":"src/ui/beamline/components/spinner/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  label: \"Loading\",\n  done: \"Done\",\n  error: \"Failed\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/spinner/spinner.module.css","type":"registry:file","target":"src/ui/beamline/components/spinner/spinner.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* With a delay the ring waits unseen and then fades in, so quick work never flashes it; the result (a check or a\n     cross) always shows at once. */\n  .root[data-delay] {\n    animation: appear var(--pui-fade-in) var(--pui-ease-default) var(--_delay) both;\n  }\n  @keyframes appear {\n    from {\n      opacity: 0;\n    }\n  }\n  /* The turn runs on the compositor (a CSS animation on the svg box) and costs no JS per frame. When the work ends the\n     arc closes into a full ring over the standard duration, so stopping the turn is invisible, and the check or cross\n     draws itself a beat later. All timing comes from the motion tokens; the loop period follows --pui-motion-scale. */\n  .svg[data-state=\"loading\"] {\n    animation: turn calc(0.8s * var(--pui-motion-scale, 1) * var(--pui-motion-look, 1)) linear infinite;\n  }\n  .track {\n    stroke-opacity: 0.18;\n  }\n  .arc {\n    stroke-dasharray: 0.3 1;\n    transition: stroke-dasharray var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .svg:not([data-state=\"loading\"]) .arc {\n    stroke-dasharray: 1 1;\n  }\n  .mark {\n    stroke-dasharray: 1 1;\n    animation: draw var(--pui-duration-standard) var(--pui-ease-enter) var(--pui-duration-fast) both;\n  }\n  @keyframes turn {\n    to {\n      transform: rotate(1turn);\n    }\n  }\n  @keyframes draw {\n    from {\n      stroke-dashoffset: 1;\n    }\n    to {\n      stroke-dashoffset: 0;\n    }\n  }\n  .svg[data-paused] {\n    animation-play-state: paused;\n  }\n  .svg[data-reduced],\n  .svg[data-reduced] .mark {\n    animation: none;\n  }\n  /* Reduction retains the loading/result shape without a decorative loop. */\n  @media (prefers-reduced-motion: reduce) {\n    .svg[data-state=\"loading\"],\n    .mark {\n      animation: none;\n    }\n  }\n}\n"},{"path":"beamline/components/spinner/spinner.tsx","type":"registry:file","target":"src/ui/beamline/components/spinner/spinner.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SpinnerDefaults } from \"./defaults\";\nexport type { SpinnerDefaults } from \"./defaults\";\n\nimport { useRef, type CSSProperties, type Ref } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./spinner.module.css\";\n\nexport interface SpinnerClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the svg part; no override by default. */\n  svg?: string;\n  /** Classes for the track part; no override by default. */\n  track?: string;\n  /** Classes for the arc part; no override by default. */\n  arc?: string;\n  /** Classes for the mark part; no override by default. */\n  mark?: string;\n}\n\nexport interface SpinnerProps {\n  /** `success` draws a check out of the ring, `error` a cross; both stop spinning. */\n  state?: \"loading\" | \"success\" | \"error\";\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Accessible name while loading. */\n  label?: string;\n  /** ms to wait before it shows: work that ends sooner never flashes a spinner (about 300 is a good start). Default 0. */\n  delay?: number;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLSpanElement>;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SpinnerClassNames;\n}\n\n/** Icon box from the foundation icon steps (§14.2): sm 14, md 16, lg 20. */\nconst PX = { sm: \"var(--pui-icon-sm)\", md: \"var(--pui-icon-md)\", lg: \"var(--pui-icon-lg)\" } as const;\n\n/**\n * A ring that spins while work runs and closes into a drawn check (or cross) when it ends — the result is a shape as\n * well as a colour. With reduced motion the loading ring holds still.\n */\nexport function Spinner({\n  state = \"loading\",\n  size: sizeProp,\n  label: labelProp,\n  delay = 0,\n  className,\n  classNames,\n  style,\n  ref,\n}: SpinnerProps) {\n  const i18n = useMessages(\"spinner\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n\n  const scopeDefaults = useComponentDefaults<SpinnerDefaults>(\"spinner\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = tokens.reduced || managedReduced;\n\n  const box = PX[size];\n  const done = state !== \"loading\";\n  const tone = state === \"success\" ? \"text-green\" : state === \"error\" ? \"text-red\" : \"text-ink-2\";\n  return (\n    <span\n      ref={attach}\n      data-slot=\"spinner\"\n      data-state={state}\n      data-delay={delay > 0 && state === \"loading\" ? \"\" : undefined}\n      role={done ? \"img\" : \"status\"}\n      aria-label={state === \"success\" ? i18n(\"done\") : state === \"error\" ? i18n(\"error\") : label}\n      className={cn(styles.root, \"relative inline-grid shrink-0 place-items-center\", tone, classNames?.root, className)}\n      style={{ width: box, height: box, \"--_delay\": `${delay}ms`, ...style } as CSSProperties}\n    >\n      <svg\n        viewBox=\"0 0 24 24\"\n        width=\"100%\"\n        height=\"100%\"\n        fill=\"none\"\n        aria-hidden=\"true\"\n        data-state={state}\n        data-slot=\"spinner-svg\"\n        data-reduced={reduced ? \"\" : undefined}\n        data-paused={active ? undefined : \"\"}\n        className={cn(styles.svg, classNames?.svg)}\n      >\n        <circle\n          data-slot=\"spinner-track\"\n          className={cn(styles.track, classNames?.track)}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"9.5\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n        />\n        <circle\n          data-slot=\"spinner-arc\"\n          className={cn(styles.arc, classNames?.arc)}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"9.5\"\n          pathLength={1}\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n          strokeLinecap=\"round\"\n          transform=\"rotate(-90 12 12)\"\n        />\n        {state === \"success\" && (\n          <path\n            data-slot=\"spinner-mark\"\n            className={cn(styles.mark, classNames?.mark)}\n            d=\"M7.5 12.5l3 3 6-6.5\"\n            pathLength={1}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        )}\n        {state === \"error\" && (\n          <path\n            data-slot=\"spinner-mark\"\n            className={cn(styles.mark, classNames?.mark)}\n            d=\"M8.5 8.5l7 7M15.5 8.5l-7 7\"\n            pathLength={1}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.5\"\n            strokeLinecap=\"round\"\n          />\n        )}\n      </svg>\n    </span>\n  );\n}\n\nexport default Spinner;\n"},{"path":"beamline/components/tooltip/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/tooltip/defaults.ts","content":"/** Pure allowed presentation defaults for Tooltip; no component or runtime imports. */\nexport interface TooltipDefaults {\n  /** Shared align setting; omit to inherit the surrounding scope or the component default. */\n  align?: \"start\" | \"center\" | \"end\";\n}\n"},{"path":"beamline/components/tooltip/tooltip.module.css","type":"registry:file","target":"src/ui/beamline/components/tooltip/tooltip.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A tooltip arrives from its control: it starts a step toward the control on the side it opens on and moves away\n     into place as it fades in (--pui-motion-in), so on any side it reads as coming out of what it explains. Inside a\n     group, the next one opens at once where the last one stood and travels to its own control (--pui-duration-fast),\n     while the last one goes in the arrival time: one tooltip moving along the toolbar, not two blinking. It leaves\n     over the departure fade. Reduced motion keeps the fades and drops the travel (--pui-motion-travel is 0). */\n  .tip {\n    --_dx: 0px;\n    --_dy: 0px;\n    --_tx: 0px;\n    --_ty: 0px;\n    z-index: var(--pui-z-tooltip);\n    transform-origin: var(--radix-tooltip-content-transform-origin);\n  }\n  .tip[data-side=\"top\"] {\n    --_dy: var(--pui-space-1);\n  }\n  .tip[data-side=\"bottom\"] {\n    --_dy: calc(-1 * var(--pui-space-1));\n  }\n  .tip[data-side=\"left\"] {\n    --_dx: var(--pui-space-1);\n  }\n  .tip[data-side=\"right\"] {\n    --_dx: calc(-1 * var(--pui-space-1));\n  }\n  .tip[data-state=\"delayed-open\"] {\n    animation: tip-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  .tip[data-state=\"instant-open\"] {\n    animation: tip-fade var(--pui-fade-in) var(--pui-ease-default);\n  }\n  .tip[data-state=\"instant-open\"][data-travel] {\n    animation: tip-travel var(--pui-duration-fast) var(--pui-ease-standard);\n  }\n  .tip[data-state=\"closed\"] {\n    animation: tip-out var(--pui-fade-out) var(--pui-ease-default) forwards;\n  }\n  .tip[data-grouped][data-state=\"closed\"] {\n    animation-duration: var(--pui-fade-in);\n  }\n}\n@keyframes tip-in {\n  from {\n    opacity: 0;\n    translate: calc(var(--_dx) * var(--pui-motion-travel, 1)) calc(var(--_dy) * var(--pui-motion-travel, 1));\n  }\n}\n@keyframes tip-fade {\n  from {\n    opacity: 0;\n  }\n}\n@keyframes tip-travel {\n  from {\n    translate: calc(var(--_tx) * var(--pui-motion-travel, 1)) calc(var(--_ty) * var(--pui-motion-travel, 1));\n  }\n}\n@keyframes tip-out {\n  to {\n    opacity: 0;\n  }\n}\n"},{"path":"beamline/components/tooltip/tooltip.tsx","type":"registry:file","target":"src/ui/beamline/components/tooltip/tooltip.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { TooltipDefaults } from \"./defaults\";\nexport type { TooltipDefaults } from \"./defaults\";\n\nimport {\n  createContext,\n  useContext,\n  useRef,\n  useState,\n  type CSSProperties,\n  type Ref,\n  type ReactElement,\n  type ReactNode,\n  type RefObject,\n} from \"react\";\nimport * as RadixTooltip from \"@radix-ui/react-tooltip\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./tooltip.module.css\";\n\n/** How long after one tooltip of a group closes the next opens at once (and travels from where the last one was). */\nconst SKIP = 400;\n/** The last control of a group whose tooltip showed: its centre and when its tooltip opened or closed. */\ntype Last = { el: Element; x: number; y: number; at: number };\nconst Grouped = createContext<RefObject<Last | null> | null>(null);\n\n/** Tooltips inside a group open instantly one after another (a toolbar): only the first waits, and each next one\n *  travels from where the last one stood. */\nexport function TooltipGroup({\n  children,\n  delay = 500,\n}: {\n  /** Content rendered inside the component. Required. */\n  children: ReactNode;\n  /** Milliseconds before the first tooltip in the group opens. Defaults to 500. */\n  delay?: number;\n}) {\n  const last = useRef<Last | null>(null);\n  return (\n    <RadixTooltip.Provider delayDuration={delay} skipDelayDuration={SKIP}>\n      <Grouped.Provider value={last}>{children}</Grouped.Provider>\n    </RadixTooltip.Provider>\n  );\n}\n\nexport interface TooltipClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the trigger part; no override by default. */\n  trigger?: string;\n  /** Classes for the shortcut part; no override by default. */\n  shortcut?: string;\n}\n\nexport interface TooltipProps {\n  /** What the control does; a few words. */\n  content: ReactNode;\n  /** A keyboard shortcut shown after the text, e.g. \"⌘B\". */\n  shortcut?: string;\n  /** The control: one element that can take a ref (a button). */\n  children: ReactElement;\n  /** Edge from which the panel or anchored content opens. Defaults to \"top\". */\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  /** Alignment within the available space. Defaults to \"center\". */\n  align?: \"start\" | \"center\" | \"end\";\n  /** Open state, controlled or uncontrolled; the tooltip opens on hover and on keyboard focus either way. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. Defaults to false. */\n  defaultOpen?: boolean;\n  /** Reports open state after a change; no callback by default. */\n  onOpenChange?: (open: boolean) => void;\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?: TooltipClassNames;\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<HTMLDivElement>;\n}\n\n/**\n * Short supporting text for a control: it opens on hover after a moment and at once on keyboard focus, and closes on\n * Escape. It arrives out of its control on whichever side it opens. Inside a TooltipGroup the next one opens instantly\n * and travels from where the last one stood. Never put essential information or anything clickable in a tooltip.\n */\nexport function Tooltip({\n  content,\n  shortcut,\n  children,\n  side = \"top\",\n  align: alignProp,\n  open,\n  defaultOpen,\n  onOpenChange,\n  className,\n  classNames,\n  style,\n  ref,\n}: TooltipProps) {\n  const scopeDefaults = useComponentDefaults<TooltipDefaults>(\"tooltip\", [\"align\"]);\n  const align = alignProp ?? scopeDefaults.align ?? \"center\";\n\n  const group = useContext(Grouped);\n  const container = usePortalContainer();\n  const trigger = useRef<HTMLButtonElement>(null); // the control, whatever element it is (Radix types it as a button)\n  // in a group, how far this tooltip is from the last one, so it can start there and travel to its own control\n  const [travel, setTravel] = useState<{ x: number; y: number } | null>(null);\n  const change = (next: boolean) => {\n    const el = trigger.current;\n    if (group && el) {\n      const r = el.getBoundingClientRect();\n      const x = r.left + r.width / 2;\n      const y = r.top + r.height / 2;\n      const last = group.current;\n      if (next) {\n        const near = last && last.el !== el && performance.now() - last.at < SKIP;\n        setTravel(near ? { x: last.x - x, y: last.y - y } : null);\n        group.current = { el, x, y, at: Infinity };\n      } else if (last?.el === el) last.at = performance.now();\n    }\n    onOpenChange?.(next);\n  };\n  const tip = (\n    <RadixTooltip.Root open={open} defaultOpen={defaultOpen} onOpenChange={change}>\n      <RadixTooltip.Trigger ref={trigger} asChild data-slot=\"tooltip-trigger\" className={classNames?.trigger}>\n        {children}\n      </RadixTooltip.Trigger>\n      {container !== null && (\n        <RadixTooltip.Portal container={container}>\n          <RadixTooltip.Content\n            data-slot=\"tooltip\"\n            data-grouped={group ? \"\" : undefined}\n            data-travel={travel ? \"\" : undefined}\n            style={travel ? ({ \"--_tx\": `${travel.x}px`, \"--_ty\": `${travel.y}px`, ...style } as CSSProperties) : style}\n            ref={ref}\n            side={side}\n            align={align}\n            sideOffset={6}\n            collisionPadding={8}\n            className={cn(\n              \"flex max-w-[min(260px,calc(100vw-16px))] items-center gap-2 rounded-chip bg-tooltip px-2 py-1 text-xs leading-snug text-tooltip-fg shadow-overlay\",\n              styles.tip,\n              classNames?.root,\n              className,\n            )}\n          >\n            {content}\n            {shortcut && (\n              <kbd\n                data-slot=\"tooltip-shortcut\"\n                className={cn(\"font-mono text-2xs text-[var(--pui-tooltip-muted)]\", classNames?.shortcut)}\n              >\n                {shortcut}\n              </kbd>\n            )}\n          </RadixTooltip.Content>\n        </RadixTooltip.Portal>\n      )}\n    </RadixTooltip.Root>\n  );\n  return group ? tip : <RadixTooltip.Provider delayDuration={500}>{tip}</RadixTooltip.Provider>;\n}\n\nexport default Tooltip;\n"},{"path":"beamline/hooks/use-active.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-active.ts","content":"\"use client\";\n\nimport { useContext, useEffect, useState, type RefObject } from \"react\";\n\nimport { ActivityContext } from \"../runtime/activity\";\n\n/**\n * Whether an animated element should be doing work: it is (nearly) on screen and the tab is visible. Every\n * continuously animated component gates its loop, shader or CSS animation on this, so an effect scrolled out of view,\n * behind another tab, or in a collapsed panel costs nothing. One IntersectionObserver per element; no polling.\n */\nexport function useActive(\n  ref: RefObject<Element | null>,\n  {\n    margin = \"120px\",\n    key,\n    idle = true,\n  }: {\n    margin?: string;\n    /** Re-attach when this changes (an element that mounts later). */\n    key?: unknown;\n    /** Pause continuous work at the host idle boundary. Finite sequences set false so they finish. */\n    idle?: boolean;\n  } = {},\n) {\n  const active = useContext(ActivityContext);\n  const [inView, setInView] = useState(false);\n  const [visible, setVisible] = useState(\n    () => typeof document === \"undefined\" || document.visibilityState === \"visible\",\n  );\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") {\n      setInView(true);\n      return;\n    }\n    const io = new IntersectionObserver(([e]) => setInView(e.isIntersecting), { rootMargin: margin });\n    io.observe(el);\n    return () => io.disconnect();\n  }, [ref, margin, key]);\n  useEffect(() => {\n    const on = () => setVisible(document.visibilityState === \"visible\");\n    document.addEventListener(\"visibilitychange\", on);\n    return () => document.removeEventListener(\"visibilitychange\", on);\n  }, []);\n  return (!idle || active) && inView && visible;\n}\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-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-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/hooks/use-fit.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-fit.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\n/**\n * How many of a row's items fit its room: the one fold every element that lists things in a line uses (chips,\n * avatars, breadcrumbs, filters, tags, actions), so a narrow panel shows fewer and a \"+N\" for the rest, and the rest\n * come back as the room grows. Content-sized items keep their natural width; the row decides how many. Items never\n * change order (a fold that reshuffles breaks the reader's memory of where things are): the last ones fold first.\n *\n * Mark each item `data-fit-item` (direct children of the row) and the overflow control (the \"+N\") `data-fit-more`.\n * Every item is measured once at its natural width — all of them render on the first pass and again whenever `items`\n * or `key` changes, read in a layout effect, so nothing is painted half-measured — and then each change of the row's\n * width lays the widths out with the row's own column gap into `lines` lines, keeping room on the last line for the\n * overflow control. Nothing runs at rest: one ResizeObserver on the row.\n *\n * `key` names the item set and must be a stable primitive (a string of the items' ids and labels), never an array or\n * object built during render: a new identity each render would measure again each render, for ever.\n * `keep[i]` marks items that always show (a chosen chip, the current crumb); room for them is taken first.\n * Returns how many of the other items show, in order (all of them while measuring, when off, and when they fit).\n */\nexport function useFit(\n  row: RefObject<HTMLElement | null>,\n  {\n    items,\n    lines = 1,\n    enabled = true,\n    min = 1,\n    keep,\n    from = \"start\",\n  }: {\n    /** How many items there are; a change (or a different `key`) measures them again. */\n    items: number;\n    /** Lines the row may take before it folds. */\n    lines?: number;\n    enabled?: boolean;\n    /** Never show fewer than this many of the other items. */\n    min?: number;\n    keep?: readonly boolean[];\n    /** Which of the other items the room shows: the first that fit (start, the default: the last fold first), or the\n     *  last that fit (end: a breadcrumb folds the levels nearest the top and keeps the parent). The count returned is\n     *  how many show either way; the caller shows them from its end. */\n    from?: \"start\" | \"end\";\n  },\n  key?: string | number,\n): number {\n  const free = keep ? keep.filter((k) => !k).length : items;\n  const [count, setCount] = useState(free);\n  const widths = useRef<number[] | null>(null);\n  const moreWidth = useRef(0);\n  const measuring = useRef(true);\n  const kept = useRef(keep);\n  kept.current = keep;\n  const end = useRef(from === \"end\");\n  end.current = from === \"end\";\n  const recent = useRef<number[]>([]);\n  const last = useRef(free);\n\n  // a new set of items: show them all for one (unpainted) pass so their natural widths can be read\n  useLayoutEffect(() => {\n    widths.current = null;\n    measuring.current = true;\n    setCount(free);\n  }, [items, key, enabled, free]);\n\n  useLayoutEffect(() => {\n    const el = row.current;\n    if (!el || !enabled || typeof ResizeObserver === \"undefined\") return;\n    const layout = () => {\n      if (!widths.current) return;\n      const cs = getComputedStyle(el);\n      const gap = parseFloat(cs.columnGap) || 0;\n      const room = roomOf(el, cs) - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0);\n      // counted from the end, the same placement is read backwards: the last items are the ones that stay\n      const w = end.current ? [...widths.current].reverse() : widths.current;\n      const k = end.current && kept.current ? [...kept.current].reverse() : kept.current;\n      let next = fit(w, room, gap, moreWidth.current, lines, min, k);\n      // a backstop: a row whose room still moves with its own fold (a layout the room reading can't see) settles on\n      // the folded side instead of flipping every frame\n      const now = performance.now();\n      const flips = recent.current.filter((t) => now - t < 500);\n      if (flips.length >= 6) next = Math.min(next, last.current);\n      if (next !== last.current) recent.current = [...flips, now];\n      last.current = next;\n      setCount((c) => (c === next ? c : next));\n    };\n    if (measuring.current) {\n      const found = [...el.querySelectorAll<HTMLElement>(\":scope > [data-fit-item]\")];\n      if (found.length >= items) {\n        widths.current = found.slice(0, items).map((n) => n.getBoundingClientRect().width);\n        const more = el.querySelector<HTMLElement>(\":scope > [data-fit-more]\");\n        // the \"+N\" is not on screen while everything fits: assume a short chip until it has been seen\n        moreWidth.current = more ? more.getBoundingClientRect().width : Math.max(moreWidth.current, 56);\n        measuring.current = false;\n      }\n    }\n    layout();\n    const observer = new ResizeObserver(() => {\n      const more = el.querySelector<HTMLElement>(\":scope > [data-fit-more]\");\n      if (more) moreWidth.current = more.getBoundingClientRect().width;\n      layout();\n    });\n    observer.observe(el);\n    // the room is the parent's line as much as the row's own box: a content-sized row only learns it can grow there\n    if (el.parentElement) observer.observe(el.parentElement);\n    return () => observer.disconnect();\n  });\n\n  return enabled ? count : free;\n}\n\n/**\n * The width a row may take: its own box, or, when it sits content-sized in a flex row (a tab row beside a title in a\n * space-between bar), the line's width less what shares that line with it. Read this way the room never depends on\n * how many items the row shows, so folding can't change the room it was decided by (and flip back).\n */\nfunction roomOf(el: HTMLElement, cs: CSSStyleDeclaration) {\n  const own = el.clientWidth;\n  const parent = el.parentElement;\n  if (!parent) return own;\n  const ps = getComputedStyle(parent);\n  if (!ps.display.includes(\"flex\") || !ps.flexDirection.startsWith(\"row\")) return own;\n  const box = el.getBoundingClientRect();\n  let others = 0;\n  let count = 0;\n  for (const sib of parent.children) {\n    if (sib === el || !(sib instanceof HTMLElement)) continue;\n    const ss = getComputedStyle(sib);\n    if (ss.position === \"absolute\" || ss.position === \"fixed\" || ss.display === \"none\") continue;\n    const r = sib.getBoundingClientRect();\n    // on the row's own line (a wrapped bar puts the others above or below)\n    if (r.bottom <= box.top + 0.5 || r.top >= box.bottom - 0.5) continue;\n    others += r.width + (parseFloat(ss.marginLeft) || 0) + (parseFloat(ss.marginRight) || 0);\n    count++;\n  }\n  const line =\n    parent.clientWidth -\n    (parseFloat(ps.paddingLeft) || 0) -\n    (parseFloat(ps.paddingRight) || 0) -\n    others -\n    count * (parseFloat(ps.columnGap) || 0) -\n    (parseFloat(cs.marginLeft) || 0) -\n    (parseFloat(cs.marginRight) || 0) -\n    // the line is measured outside the row's border; clientWidth is inside it\n    (parseFloat(cs.borderLeftWidth) || 0) -\n    (parseFloat(cs.borderRightWidth) || 0);\n  const cap = cs.maxWidth.endsWith(\"px\") ? parseFloat(cs.maxWidth) : Infinity;\n  return Math.max(own, Math.min(line, cap));\n}\n\n/**\n * How many of the items not kept fit, in order, when the shown ones are laid into `lines` lines of `room` with `gap`\n * between them and `more` kept free on the last line whenever something folds.\n */\nexport function fit(\n  widths: readonly number[],\n  room: number,\n  gap: number,\n  more: number,\n  lines: number,\n  min: number,\n  keep?: readonly boolean[],\n) {\n  const fits = (show: (i: number) => boolean, reserve: number) => {\n    let line = 1;\n    let x = 0;\n    for (let i = 0; i < widths.length; i++) {\n      if (!show(i)) continue;\n      const w = widths[i];\n      const cap = line === lines ? room - reserve : room;\n      const need = x === 0 ? w : x + gap + w;\n      if (need <= cap + 0.5) {\n        x = need;\n        continue;\n      }\n      if (x === 0 || line === lines) return false;\n      line++;\n      if (w > (line === lines ? room - reserve : room) + 0.5) return false;\n      x = w;\n    }\n    return true;\n  };\n  const rank: number[] = [];\n  let free = 0;\n  for (let i = 0; i < widths.length; i++) rank.push(keep?.[i] ? -1 : free++);\n  if (fits(() => true, 0)) return free;\n  // with items that always show, the floor is none of the others (they are what keeps the row from being empty)\n  const floor = keep?.some(Boolean) ? 0 : Math.min(min, free);\n  for (let n = free - 1; n > floor; n--) if (fits((i) => rank[i] < 0 || rank[i] < n, more + gap)) return n;\n  return floor;\n}\n"},{"path":"beamline/hooks/use-flip.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-flip.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, type RefObject } from \"react\";\nimport { useMotionTokens } from \"./use-motion-tokens\";\nimport { cssEase } from \"../lib/motion-tokens\";\n\ntype Seen = { el: HTMLElement; x: number; y: number; w: number; h: number };\n\n/**\n * Items of a collection keep their path (the morph grammar's \"changes keep their size and their path\"): mark each item\n * with `data-flip=\"<stable key>\"` inside `container`. After a change, an item that is new grows in, items that moved\n * slide from where they were, and an item that left fades out as a ghost where it stood (a still copy, inert and\n * hidden from assistive tech, removed when it has faded). The data and focus change at once; only the picture\n * settles. Nothing runs at rest; reduced motion places at once. Positions are kept in the container's own content\n * (relative to it, plus its scroll) and read at rest, so neither a scroll of the page, nor a scroll of the container\n * itself, nor an item still settling reads as movement.\n *\n * `resize`: an item whose box changed size also scales from its old box (for marks without text, a bar or a segment, so a\n * new value glides on transforms instead of laying out every frame). `origin(key)`: an element a new item grows out of\n * (a ribbon's segment becoming a column, a shared chip dividing into pieces); with `originFirst` it applies on the\n * container's first render too, so a panel that opens unfolds its items from what was already on screen. `inner`: items\n * inside another item that lies outside the container (a merge's rows inside a card that moves itself) are this\n * container's own; nesting is judged inside the container only.\n */\nexport function useFlip(\n  container: RefObject<HTMLElement | null> | (() => HTMLElement | null),\n  {\n    ghost = true,\n    enter = true,\n    resize = false,\n    origin,\n    originFirst = false,\n    inner = false,\n  }: {\n    ghost?: boolean;\n    enter?: boolean;\n    resize?: boolean;\n    origin?: (key: string) => Element | null | undefined;\n    originFirst?: boolean;\n    inner?: boolean;\n  } = {},\n) {\n  const get = typeof container === \"function\" ? container : () => container.current;\n  // timing read at the container itself, found when the commit has attached it (a function container has no ref of its\n  // own; without a host the timing stayed unresolved where no UIRoot is around, then rendered again to say so)\n  const host =\n    typeof container === \"function\"\n      ? {\n          get current() {\n            return container();\n          },\n        }\n      : container;\n  const motion = useMotionTokens(host);\n  const last = useRef<Map<string, Seen> | null>(null);\n  const lastWidth = useRef(0);\n  const flying = useRef(new Set<Animation>());\n  // where every item stands, in the container's own content (relative to it, plus its scroll): a list scrolled since the\n  // last render has moved nothing (a row that slid back by the scroll would leave the finger that is tapping it). The\n  // items' own grow-ins and slides still in flight are lifted while they are read, so each box is where the layout puts\n  // it: a render mid-flight (a live price ticking while a new row grows in) is not a move.\n  const read = (root: HTMLElement) => {\n    const lifted = [...flying.current].map((a) => a.effect as KeyframeEffect).map((e) => [e, e.target] as const);\n    for (const [effect] of lifted) effect.target = null;\n    const box = root.getBoundingClientRect();\n    const now = new Map<string, Seen>();\n    for (const el of root.querySelectorAll<HTMLElement>(\"[data-flip]\")) {\n      // only the outermost items move (with `inner`, outermost inside this container)\n      const outer = el.parentElement?.closest(\"[data-flip]\");\n      if (outer && (!inner || (outer !== root && root.contains(outer)))) continue;\n      const r = el.getBoundingClientRect();\n      now.set(el.dataset.flip!, {\n        el,\n        x: r.left - box.left + root.scrollLeft,\n        y: r.top - box.top + root.scrollTop,\n        w: r.width,\n        h: r.height,\n      });\n    }\n    for (const [effect, target] of lifted) effect.target = target;\n    return { box, now };\n  };\n  // a container that changes size at rest, with no render (fonts arriving, a scrollbar appearing, a window resized), has\n  // its places read again then: else the first change after it would take the new width for a reflow and move nothing\n  const watched = useRef<{ el: HTMLElement; observer: ResizeObserver } | null>(null);\n  useLayoutEffect(() => () => watched.current?.observer.disconnect(), []);\n  // every render on purpose: a collection changes through its parent's state, not through anything we can depend on\n  useLayoutEffect(() => {\n    const root = get();\n    // a container that is gone (a panel folded) starts over when it comes back: its items are new, not moved\n    if (!root) {\n      last.current = null;\n      return;\n    }\n    if (watched.current?.el !== root && typeof ResizeObserver !== \"undefined\") {\n      watched.current?.observer.disconnect();\n      const observer = new ResizeObserver(() => {\n        if (flying.current.size || !last.current || !root.isConnected) return;\n        const seen = read(root);\n        last.current = seen.now;\n        lastWidth.current = seen.box.width;\n      });\n      observer.observe(root);\n      watched.current = { el: root, observer };\n    }\n    const { box, now } = read(root);\n    // a container that changed width since the last render reflowed its items by itself (a panel resized, a room\n    // step crossed): that is placement, not a change a person made, so this render records the new places and moves\n    // nothing (a reflow places, only a choice travels)\n    const reflowed = Math.abs(box.width - lastWidth.current) > 0.5;\n    lastWidth.current = box.width;\n    const before = reflowed ? null : last.current;\n    last.current = now;\n    const ms = motion.duration.standard * 1000;\n    // the first render moves nothing, except items that grow out of an origin already on screen\n    if ((!before && !(origin && originFirst)) || motion.reduced || ms < 2) return;\n    const easing = cssEase(motion.ease.standard, \"ease-out\");\n    // an item in flight says so (`data-flip-flight`: \"far\" when it crosses more than its own size, a slide between groups or\n    // a share growing out of another item; \"near\" for a neighbour's step), so a part can lift what travels over other\n    // text onto a surface of its own instead of drawing words over words\n    const fly = (el: HTMLElement, keyframes: Keyframe[], far = false) => {\n      const flight = el.animate(keyframes, { duration: ms, easing });\n      flying.current.add(flight);\n      if (far || el.dataset.flipFlight !== \"far\") el.dataset.flipFlight = far ? \"far\" : \"near\";\n      const land = () => {\n        flying.current.delete(flight);\n        if (![...flying.current].some((a) => (a.effect as KeyframeEffect | null)?.target === el))\n          delete el.dataset.flipFlight;\n      };\n      flight.finished.then(land, land);\n    };\n    const boxOf = (key: string) => {\n      const from = origin?.(key);\n      if (!from || !from.isConnected) return null;\n      const r = from.getBoundingClientRect();\n      return { x: r.left - box.left + root.scrollLeft, y: r.top - box.top + root.scrollTop, w: r.width, h: r.height };\n    };\n    // from a box to where the item stands: translate and scale about the top-left corner\n    const morph = (was: { x: number; y: number; w: number; h: number }, at: Seen): Keyframe[] => [\n      {\n        transformOrigin: \"0 0\",\n        transform: `translate(${was.x - at.x}px, ${was.y - at.y}px) scale(${was.w / Math.max(at.w, 0.5)}, ${was.h / Math.max(at.h, 0.5)})`,\n      },\n      { transformOrigin: \"0 0\", transform: \"none\" },\n    ];\n    for (const [key, at] of now) {\n      const was = before?.get(key);\n      if (!was) {\n        const from = boxOf(key);\n        if (from) fly(at.el, morph(from, at), true);\n        else if (enter && before)\n          fly(at.el, [\n            { opacity: 0, transform: \"scale(0.86)\" },\n            { opacity: 1, transform: \"none\" },\n          ]);\n        continue;\n      }\n      const dx = was.x - at.x,\n        dy = was.y - at.y;\n      const resized = resize && (Math.abs(was.w - at.w) > 0.5 || Math.abs(was.h - at.h) > 0.5);\n      const far = Math.abs(dx) > at.w * 0.5 || Math.abs(dy) > at.h * 1.5;\n      if (resized) fly(at.el, morph(was, at), far);\n      else if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5)\n        fly(at.el, [{ transform: `translate(${dx}px, ${dy}px)` }, { transform: \"none\" }], far);\n    }\n    if (!ghost || !before) return;\n    for (const [key, was] of before) {\n      if (now.has(key) || was.el.isConnected) continue;\n      const copy = was.el.cloneNode(true) as HTMLElement;\n      copy.removeAttribute(\"data-flip\");\n      // the ghost is a picture of the item, not the item: nothing that finds parts by slot or id may find it\n      for (const n of [copy, ...copy.querySelectorAll(\"[id], [data-slot]\")]) {\n        n.removeAttribute(\"id\");\n        n.removeAttribute(\"data-slot\");\n      }\n      copy.setAttribute(\"data-ghost\", \"\");\n      copy.setAttribute(\"aria-hidden\", \"true\");\n      copy.inert = true;\n      if (getComputedStyle(root).position === \"static\") root.style.position = \"relative\";\n      Object.assign(copy.style, {\n        position: \"absolute\",\n        left: `${was.x}px`,\n        top: `${was.y}px`,\n        width: `${was.w}px`,\n        height: `${was.h}px`,\n        margin: \"0\",\n        pointerEvents: \"none\",\n      });\n      root.appendChild(copy);\n      // the ghost is gone by the time a neighbour slides into its place, so their texts never mix. The clock runs\n      // straight and an even curve shapes only the fade: the front-loaded standard curve on the whole timeline passed\n      // 0.45 inside the first frame, so what left read as a pop instead of a fade\n      const fade = copy.animate(\n        [\n          { opacity: 1, transform: \"none\", easing: cssEase(motion.ease.default, \"ease-in-out\") },\n          { opacity: 0, transform: \"scale(0.9)\", offset: 0.6 },\n          { opacity: 0, transform: \"scale(0.86)\" },\n        ],\n        {\n          duration: motion.duration.standard * 1000,\n          easing: \"linear\",\n        },\n      );\n      const drop = () => copy.remove();\n      fade.finished.then(drop, drop);\n      // and by the clock as well: a fade that never reports its end (the page hidden, an animation cancelled from\n      // outside) must not leave a copy behind for a person or a test to find\n      setTimeout(drop, motion.duration.standard * 1000 + 250);\n    }\n  });\n}\n"},{"path":"beamline/hooks/use-media-query.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-media-query.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// one MediaQueryList per query: React reads the snapshot on every render, and a part that renders thirty times a\n// second (a playing audio player's clock) would otherwise ask the browser for a new list each time\nconst lists = new Map<string, MediaQueryList>();\nconst list = (query: string) => {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return undefined;\n  let m = lists.get(query);\n  if (!m) lists.set(query, (m = window.matchMedia(query)));\n  return m;\n};\n\n/** True while the media query matches; false on the server and in the first client render of SSR output. */\nexport function useMediaQuery(query: string): boolean {\n  return useSyncExternalStore(\n    (notify) => {\n      const m = list(query);\n      m?.addEventListener(\"change\", notify);\n      return () => m?.removeEventListener(\"change\", notify);\n    },\n    () => list(query)?.matches ?? false,\n    () => false,\n  );\n}\n\n/** Phone-width layouts: below 768px. */\nexport const usePhone = () => useMediaQuery(\"(max-width: 767px)\");\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/hooks/use-reduced-motion-safe.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-reduced-motion-safe.ts","content":"\"use client\";\n\nimport { useMediaQuery } from \"./use-media-query\";\nimport { useScopeMotion } from \"../runtime/appearance\";\n\n/** Managed motion policy plus the OS preference, with a stable server snapshot. */\nexport function useReducedMotionSafe() {\n  const requested = useScopeMotion();\n  const os = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n  return requested !== \"auto\" || os;\n}\n"},{"path":"beamline/lib/focus.ts","type":"registry:file","target":"src/ui/beamline/lib/focus.ts","content":"/**\n * Keeping a keyboard user's place when something disappears. Removing the element that has focus (a dismissed alert,\n * a removed chip or file, a deleted row) drops focus on <body>, and the next Tab starts at the top of the page.\n */\n\nconst TABBABLE = \"a[href], button, input:not([type=hidden]), select, textarea, [tabindex], [contenteditable=true]\";\n\nfunction tabbables(scope: Element) {\n  // a modal layer (menu, dialog) marks the rest of the page aria-hidden while it is open (data-aria-hidden): that\n  // content is still where focus should land once the layer closes\n  return [...scope.querySelectorAll<HTMLElement>(TABBABLE)].filter(\n    (e) =>\n      e.tabIndex >= 0 &&\n      !e.matches(\":disabled\") &&\n      !e.closest(\"[inert], [aria-hidden=true]:not([data-aria-hidden])\") &&\n      e.getClientRects().length > 0,\n  );\n}\n\n/** The control after `item` in reading order (outside it), or the one before it. */\nfunction neighbour(item: Element, scope: Element) {\n  const all = tabbables(scope).filter((e) => !item.contains(e));\n  return (\n    all.find((e) => item.compareDocumentPosition(e) & Node.DOCUMENT_POSITION_FOLLOWING) ??\n    all.reverse().find((e) => item.compareDocumentPosition(e) & Node.DOCUMENT_POSITION_PRECEDING) ??\n    null\n  );\n}\n\n/**\n * Call just before `item` is removed or hidden: if focus is inside it, focus moves to the next control after it (or\n * the one before). `scope` keeps the search inside a widget (a toolbar, a field); `fallback` is used when the scope has\n * nothing else to focus. Does nothing when focus is elsewhere, so a removal the user did not start never steals focus.\n */\nexport function focusAfterRemoval(\n  item: Element | null | undefined,\n  { scope, fallback }: { scope?: Element | null; fallback?: HTMLElement | null } = {},\n) {\n  if (!item || !item.contains(document.activeElement)) return;\n  (neighbour(item, scope ?? document.body) ?? fallback)?.focus();\n}\n\n/**\n * For an action that may or may not remove its item (the app decides, as with a row's Delete): call before running it.\n * If the item is gone afterwards and focus fell to the page, focus moves to the control that followed it.\n */\nexport function keepPlaceIfRemoved(item: Element | null | undefined, { scope }: { scope?: Element | null } = {}) {\n  if (!item) return;\n  const target = neighbour(item, scope ?? document.body);\n  const check = () => {\n    if (\n      !item.isConnected &&\n      (!document.activeElement || document.activeElement === document.body) &&\n      target?.isConnected\n    )\n      target.focus();\n  };\n  setTimeout(check, 0);\n  setTimeout(check, 400); // after an exit animation\n}\n\n/**\n * A press on a part's surface that hands focus to its control (a slider's well, a mark under its track): call from the\n * pointerdown. Focus goes to `control` without the keyboard's ring (`focusVisible: false`, so the part does not switch\n * to its keyboard look after a click). A mouse press is also followed by the browser's own focus step, which moves\n * focus to the nearest focusable ancestor or the page and leaves the arrow keys dead after the click; that step is\n * cancelled for this one press (and the press selects no text). Cancelling the pointerdown instead would also hide the\n * click from the browser, which then reads the focus as the keyboard's. A control a keyboard user already focused keeps\n * its ring.\n */\nexport function focusFromPress(\n  control: HTMLElement | null | undefined,\n  event: PointerEvent | { nativeEvent: PointerEvent },\n) {\n  if (!control) return;\n  const pointer = \"nativeEvent\" in event ? event.nativeEvent : event;\n  // asserted, not declared globally: shipped source must type-check without this repo's globals.d.ts, on the\n  // TypeScript an app already has (create-next-app installs 5.9, whose DOM lib lacks focusVisible)\n  control.focus({ preventScroll: true, focusVisible: false } as FocusOptions);\n  // the mouse's compatibility events follow the pointerdown in the same task; a finger's arrive only after a tap\n  if (pointer.pointerType !== \"mouse\") return;\n  const cancel = (e: MouseEvent) => e.preventDefault();\n  window.addEventListener(\"mousedown\", cancel, { capture: true, once: true });\n  setTimeout(() => window.removeEventListener(\"mousedown\", cancel, { capture: true }), 0);\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":"Multi-select is in src/ui/beamline/components/multi-select/multi-select.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/multi-select"}}