{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"search-field","type":"registry:component","title":"Search field","description":"The search entry point: magnifier, clear button, Escape to clear, a shortcut hint and an optional live border beam.","categories":["forms"],"dependencies":["@radix-ui/react-direction@1.1.4","@radix-ui/react-label@2.1.15","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/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/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/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/glow/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/glow/defaults.ts","content":"/** Pure allowed presentation defaults for Glow; no component or runtime imports. */\nexport interface GlowDefaults {\n  /** Shared effect setting; omit to inherit the component default. Scoped defaults retain the legacy key variant. */\n  variant?: \"aurora\" | \"ring\";\n}\n"},{"path":"beamline/components/glow/glow.module.css","type":"registry:file","target":"src/ui/beamline/components/glow/glow.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Light that says \"working\". Aurora: colour pools under and behind the element and drifts. Ring: a band of colour\n   travels around the edge. Both are blurred layers that only translate or rotate (compositor work), fade in and out\n   with `active`, stop entirely when inactive, and hold still off screen. Colours come from --pui-glow-1/2/3; every\n   period is a multiple of --pui-duration-considered, so the scope's animation speed reaches the light too. */\n  .root {\n    --pui-r: var(--pui-radius-card);\n    --pui-i: 1;\n    position: relative;\n    isolation: isolate;\n    border-radius: var(--pui-r);\n  }\n  .root > .field {\n    position: absolute;\n    z-index: -1;\n    pointer-events: none;\n    opacity: 0;\n    transition: opacity calc(var(--pui-duration-considered) * 2) var(--pui-ease-standard);\n  }\n  .root[data-active] > .field {\n    opacity: calc(0.9 * var(--pui-i));\n  }\n\n  /* aurora: three soft blobs along the lower edge, each drifting on its own period */\n  .aurora {\n    inset: 30% -6% -34%;\n    filter: blur(22px);\n  }\n  .blob {\n    position: absolute;\n    bottom: 0;\n    width: 46%;\n    height: 70%;\n    border-radius: 50%;\n    will-change: transform;\n  }\n  .blob:nth-child(1) {\n    left: 2%;\n    background: var(--pui-glow-1);\n    animation: glow-drift-a calc(var(--pui-duration-considered) * 19.5) var(--pui-ease-in-out) infinite alternate;\n  }\n  .blob:nth-child(2) {\n    left: 30%;\n    background: var(--pui-glow-2);\n    animation: glow-drift-b calc(var(--pui-duration-considered) * 25) var(--pui-ease-in-out) infinite alternate;\n  }\n  .blob:nth-child(3) {\n    right: 2%;\n    background: var(--pui-glow-3);\n    animation: glow-drift-c calc(var(--pui-duration-considered) * 22) var(--pui-ease-in-out) infinite alternate;\n  }\n\n  /* ring: a conic band (a square as wide as the element's diagonal, set from its size) rotating inside a mask that leaves only a thin frame plus its soft bloom */\n  .ring {\n    inset: -1px;\n    border-radius: calc(var(--pui-r) + 1px);\n    padding: 1.5px;\n    overflow: hidden;\n    -webkit-mask:\n      linear-gradient(#000 0 0) content-box exclude,\n      linear-gradient(#000 0 0);\n    mask:\n      linear-gradient(#000 0 0) content-box exclude,\n      linear-gradient(#000 0 0);\n  }\n  .bloom {\n    inset: -10px;\n    border-radius: calc(var(--pui-r) + 10px);\n    overflow: hidden;\n    filter: blur(12px);\n  }\n  .band {\n    position: absolute;\n    left: 50%;\n    top: 50%;\n    width: var(--pui-d, 150%);\n    aspect-ratio: 1;\n    translate: -50% -50%;\n    will-change: transform;\n    background: conic-gradient(\n      from 0deg,\n      transparent 0 58%,\n      var(--pui-glow-1) 70%,\n      var(--pui-glow-2) 80%,\n      var(--pui-glow-3) 88%,\n      transparent 96%\n    );\n    animation: glow-turn calc(var(--pui-duration-considered) * 10) linear infinite;\n  }\n  .bloom .band {\n    opacity: 0.55;\n  }\n\n  .root:not([data-active]) .blob,\n  .root:not([data-active]) .band {\n    animation-play-state: paused;\n  }\n  .root[data-paused] .blob,\n  .root[data-paused] .band {\n    animation-play-state: paused;\n  }\n  .root[data-reduced] .blob,\n  .root[data-reduced] .band {\n    animation: none;\n  }\n  @media (prefers-reduced-motion: reduce) {\n    .blob,\n    .band {\n      animation: none !important;\n    }\n  }\n}\n@keyframes glow-drift-a {\n  to {\n    transform: translate(18%, -12%) scale(1.15);\n  }\n}\n@keyframes glow-drift-b {\n  to {\n    transform: translate(-14%, 8%) scale(0.9);\n  }\n}\n@keyframes glow-drift-c {\n  to {\n    transform: translate(-20%, -10%) scale(1.1);\n  }\n}\n@keyframes glow-turn {\n  to {\n    transform: rotate(1turn);\n  }\n}\n"},{"path":"beamline/components/glow/glow.tsx","type":"registry:file","target":"src/ui/beamline/components/glow/glow.tsx","content":"\"use client\";\n\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { GlowDefaults } from \"./defaults\";\nexport type { GlowDefaults } from \"./defaults\";\n\nimport { useEffect, useRef, type CSSProperties, type ReactNode, type Ref } from \"react\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./glow.module.css\";\n\nexport interface GlowClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the field part; no override by default. */\n  field?: string;\n  /** Classes for the blob part; no override by default. */\n  blob?: string;\n  /** Classes for the bloom part; no override by default. */\n  bloom?: string;\n  /** Classes for the ring part; no override by default. */\n  ring?: string;\n  /** Classes for the band part; no override by default. */\n  band?: string;\n}\n\nexport interface GlowProps {\n  /** Content rendered inside the component. Defaults to no children. */\n  children?: ReactNode;\n  /** While true the glow fades in and moves; false fades it out and stops all motion. */\n  active?: boolean;\n  /** Aurora light behind the content or a ring around it; defaults to aurora. */\n  effect?: \"aurora\" | \"ring\";\n  /** Legacy effect setting; omitted by default. @deprecated Use effect instead. */\n  variant?: \"aurora\" | \"ring\";\n  /** 0–1, how strong the light is. */\n  intensity?: number;\n  /** Corner radius of the wrapped element, so the ring follows it. Defaults to the card radius. */\n  radius?: 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  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: GlowClassNames;\n}\n\n/**\n * Ambient light around one element that says \"something is happening here\": the composer while an agent works, the\n * live card, the call in progress. One running per screen (bind `active` to a state that ends). Compositor-only motion; nothing runs while inactive or off screen.\n */\nexport function Glow({\n  children,\n  active = true,\n  effect: effectProp,\n  variant: variantProp,\n  intensity = 1,\n  radius,\n  className,\n  classNames,\n  style,\n  ref,\n}: GlowProps) {\n  const scopeDefaults = useComponentDefaults<GlowDefaults>(\"glow\", [\"variant\"]);\n  useDeprecatedProp(\n    \"glow\",\n    \"variant\",\n    \"effect\",\n    variantProp !== undefined || (effectProp === undefined && scopeDefaults.variant !== undefined),\n  );\n  const variant = effectProp ?? variantProp ?? scopeDefaults.variant ?? \"aurora\";\n\n  const own = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(own, ref);\n  const tokens = useMotionTokens(own);\n  const managedReduced = useReducedMotionSafe();\n  const onScreen = useActive(own);\n  // The ring's rotating band must cover the element's diagonal and no more (a larger layer only costs GPU memory).\n  useEffect(() => {\n    const el = own.current;\n    if (!el || variant !== \"ring\" || typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => {\n      const { width, height } = e.contentRect;\n      el.style.setProperty(\"--pui-d\", `${Math.ceil(Math.hypot(width, height) + 24)}px`);\n    });\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [variant]);\n  return (\n    <div\n      ref={attach}\n      data-slot=\"glow\"\n      data-variant={variant}\n      data-active={active || undefined}\n      data-paused={onScreen ? undefined : \"\"}\n      data-reduced={tokens.reduced || managedReduced ? \"\" : undefined}\n      className={cn(styles.root, classNames?.root, className)}\n      style={{ ...(radius ? { \"--pui-r\": radius } : {}), \"--pui-i\": intensity, ...style } as CSSProperties}\n    >\n      {variant === \"aurora\" ? (\n        <span data-slot=\"glow-field\" aria-hidden=\"true\" className={cn(styles.field, styles.aurora, classNames?.field)}>\n          <span data-slot=\"glow-blob\" className={cn(styles.blob, classNames?.blob)} />\n          <span data-slot=\"glow-blob\" className={cn(styles.blob, classNames?.blob)} />\n          <span data-slot=\"glow-blob\" className={cn(styles.blob, classNames?.blob)} />\n        </span>\n      ) : (\n        <>\n          <span\n            data-slot=\"glow-bloom\"\n            aria-hidden=\"true\"\n            className={cn(styles.field, styles.bloom, classNames?.field, classNames?.bloom)}\n          >\n            <span data-slot=\"glow-band\" className={cn(styles.band, classNames?.band)} />\n          </span>\n          <span\n            data-slot=\"glow-ring\"\n            aria-hidden=\"true\"\n            className={cn(styles.field, styles.ring, classNames?.field, classNames?.ring)}\n          >\n            <span data-slot=\"glow-band\" className={cn(styles.band, classNames?.band)} />\n          </span>\n        </>\n      )}\n      {children}\n    </div>\n  );\n}\n\nexport default Glow;\n"},{"path":"beamline/components/kbd/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/kbd/defaults.ts","content":"/** Pure allowed presentation defaults for Kbd; no component or runtime imports. */\nexport interface KbdDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/kbd/kbd.module.css","type":"registry:file","target":"src/ui/beamline/components/kbd/kbd.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Keycaps: each key is a small raised control (the control role), so it reads as a key in every look. */\n  .keys {\n    display: inline-flex;\n    align-items: center;\n    gap: var(--pui-space-0-5);\n    vertical-align: middle;\n    /* a key combination is a fixed sequence: it reads Ctrl K in any page direction, isolated as one unit while the\n       page keeps its own direction around it */\n    direction: ltr;\n    unicode-bidi: isolate;\n  }\n  .cap {\n    display: inline-flex;\n    height: 22px;\n    min-width: 22px;\n    align-items: center;\n    justify-content: center;\n    padding-inline: var(--pui-space-1-5);\n    /* nesting input (O1 §14.1, as Button): a parent that holds this near its rounded corner sets --pui-nest-radius and\n       --pui-nest-inset; the corner becomes radius − inset, clamped between a floor (chip) and the part's own role radius (or --pui-nest-ceiling: a pill container sets it to the pill radius). Unset, the declaration is invalid and the own corner\n       applies. --pui-kbd-radius wins over both. */\n    --_kbd-nest: clamp(\n      calc(var(--pui-radius-chip) / 2),\n      calc(var(--pui-nest-radius) - var(--pui-nest-inset, 0px)),\n      var(--pui-nest-ceiling, var(--pui-radius-chip))\n    );\n    border-radius: var(--pui-kbd-radius, var(--_kbd-nest, calc(var(--pui-radius-chip) - 1px)));\n    color: var(--pui-ink-2);\n    font-size: var(--pui-text-2xs);\n    font-weight: var(--pui-weight-medium);\n    transition:\n      translate var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      var(--m-transition);\n  }\n  /* live: a held key presses its cap down at once (the look's own press light) and lets it up over the leave\n     duration; with every key of the combination held, the caps ink in the accent and take an accent ring */\n  .cap[data-pressed] {\n    --pui-m-press: 1;\n    translate: 0 1px;\n    color: var(--pui-ink);\n    transition-duration: 0ms;\n  }\n  .keys[data-complete] .cap {\n    color: var(--pui-accent-ink);\n    box-shadow:\n      var(--m-control-edge),\n      0 0 0 1px var(--pui-accent);\n  }\n  /* a finished step of a sequence keeps its caps inked while the next is awaited */\n  .cap[data-done] {\n    color: var(--pui-accent-ink);\n  }\n  /* \"then\" between the steps of a sequence: a word, seen and spoken, so G then A never reads as G and A held together.\n     While the next step is awaited a line under it runs out over the time it has (SEQUENCE_MS in kbd.tsx): a timer,\n     so reduced motion keeps it, as HoldToConfirm keeps its fill. */\n  .then {\n    position: relative;\n    padding-inline: var(--pui-space-0-5);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-2xs);\n    font-weight: var(--pui-weight-normal);\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .then[data-waiting] {\n    color: var(--pui-ink-2);\n    transition-duration: 0ms;\n  }\n  .then[data-waiting]::after {\n    content: \"\";\n    position: absolute;\n    inset-inline: var(--pui-space-0-5);\n    bottom: -2px;\n    height: 1px;\n    background: var(--pui-accent);\n    transform-origin: left;\n    /* the same wait the sequence listener keeps (SEQUENCE_MS), passed in so the line and the timeout never drift */\n    animation: kbd-wait var(--_wait) linear forwards;\n  }\n  @keyframes kbd-wait {\n    from {\n      scale: 1 1;\n    }\n    to {\n      scale: 0 1;\n    }\n  }\n  .sm {\n    height: 18px;\n    min-width: 18px;\n    padding-inline: var(--pui-space-1);\n    font-size: var(--pui-text-2xs);\n  }\n  .lg {\n    height: 28px;\n    min-width: 28px;\n    padding-inline: var(--pui-space-2);\n    font-size: var(--pui-text-sm);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.cap {\n  composes: m-control from global;\n}\n"},{"path":"beamline/components/kbd/kbd.tsx","type":"registry:file","target":"src/ui/beamline/components/kbd/kbd.tsx","content":"\"use client\";\n\nimport { useMessages, type Translator } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { KbdDefaults } from \"./defaults\";\nexport type { KbdDefaults } from \"./defaults\";\n\nimport { Fragment, useEffect, useReducer } from \"react\";\nimport type { CSSProperties, HTMLAttributes, ReactNode, Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { useApplePlatform } from \"../../hooks/use-apple-platform\";\nimport styles from \"./kbd.module.css\";\n\nconst MAP_MAC = (i18n: Translator<typeof englishMessages>): Record<string, string> => ({\n  mod: \"⌘\",\n  cmd: \"⌘\",\n  ctrl: \"⌃\",\n  alt: \"⌥\",\n  option: \"⌥\",\n  shift: \"⇧\",\n  enter: \"↵\",\n  backspace: \"⌫\",\n  esc: i18n(\"escapeShort\"),\n  tab: \"⇥\",\n  up: \"↑\",\n  down: \"↓\",\n  left: \"←\",\n  right: \"→\",\n  space: i18n(\"space\"),\n});\nconst MAP_PC = (i18n: Translator<typeof englishMessages>): Record<string, string> => ({\n  mod: i18n(\"controlShort\"),\n  cmd: i18n(\"controlShort\"),\n  ctrl: i18n(\"controlShort\"),\n  alt: i18n(\"alt\"),\n  option: i18n(\"alt\"),\n  shift: i18n(\"shift\"),\n  enter: i18n(\"enter\"),\n  backspace: i18n(\"backspace\"),\n  esc: i18n(\"escapeShort\"),\n  tab: i18n(\"tab\"),\n  up: \"↑\",\n  down: \"↓\",\n  left: \"←\",\n  right: \"→\",\n  space: i18n(\"space\"),\n});\nconst SPOKEN = (i18n: Translator<typeof englishMessages>): Record<string, string> => ({\n  \"⌘\": i18n(\"command\"),\n  \"⌃\": i18n(\"control\"),\n  \"⌥\": i18n(\"option\"),\n  \"⇧\": i18n(\"shift\"),\n  \"↵\": i18n(\"enter\"),\n  \"⌫\": i18n(\"backspace\"),\n  \"⇥\": i18n(\"tab\"),\n  \"↑\": i18n(\"up\"),\n  \"↓\": i18n(\"down\"),\n  \"←\": i18n(\"left\"),\n  \"→\": i18n(\"right\"),\n});\n\n/** The KeyboardEvent.key a cap stands for, compared in lower case. */\nconst EVENT_KEY: Record<string, string> = {\n  ctrl: \"control\",\n  alt: \"alt\",\n  option: \"alt\",\n  shift: \"shift\",\n  enter: \"enter\",\n  backspace: \"backspace\",\n  esc: \"escape\",\n  tab: \"tab\",\n  up: \"arrowup\",\n  down: \"arrowdown\",\n  left: \"arrowleft\",\n  right: \"arrowright\",\n  space: \" \",\n};\nconst eventKey = (k: string, mac: boolean) => {\n  const name = k.toLowerCase();\n  if (name === \"mod\" || name === \"cmd\") return mac ? \"meta\" : \"control\";\n  return EVENT_KEY[name] ?? name;\n};\nconst NONE: ReadonlySet<string> = new Set();\n/** The key between the steps of a sequence: [\"g\", \"then\", \"a\"] is G, then A. */\nconst THEN = \"then\";\n/** How long a sequence waits for its next step, as apps that bind them do. */\nconst SEQUENCE_MS = 1500;\n\n/** live: the keys of the shortcut held now, the step being waited for, whether the whole shortcut is done, and how\n *  many steps have been done (a step done again, G pressed twice, gives the next one its full time again). */\ninterface Live {\n  held: ReadonlySet<string>;\n  step: number;\n  complete: boolean;\n  done: number;\n}\ntype LiveAction =\n  { type: \"down\"; key: string; steps: string[][] } | { type: \"up\"; key: string; steps: string[][] } | { type: \"reset\" };\nconst REST: Live = { held: NONE, step: 0, complete: false, done: 0 };\n\n/** A step is done by the press that completes it: every key of the step held, the last one just pressed. */\nconst advance = (live: Live, key: string, steps: string[][]): Live => {\n  const want = steps[live.step];\n  if (!want.includes(key) || !want.every((k) => live.held.has(k))) return live;\n  return live.step === steps.length - 1\n    ? { ...live, complete: true }\n    : { ...live, step: live.step + 1, done: live.done + 1 };\n};\nfunction track(live: Live, action: LiveAction): Live {\n  if (action.type === \"reset\") return live === REST ? live : REST;\n  const { key, steps } = action;\n  if (action.type === \"down\") {\n    // a key outside the shortcut breaks a sequence half-way; a combination simply waits for its own keys\n    if (!steps.some((s) => s.includes(key))) return live.step || live.complete ? { ...REST, held: live.held } : live;\n    const held = live.held.has(key) ? live.held : new Set(live.held).add(key);\n    const next = advance({ ...live, held }, key, steps);\n    if (next.step === live.step && !next.complete && live.step > 0 && !steps[live.step].includes(key))\n      // a wrong key mid-sequence starts it over, and may itself begin it\n      return advance({ ...live, held, step: 0, complete: false }, key, steps);\n    return next;\n  }\n  // with ⌘ held a Mac sends no keyup for the other keys: letting go of ⌘ lets go of all of them\n  let held = live.held;\n  if (key === \"meta\") held = NONE;\n  else if (held.has(key)) {\n    const next = new Set(held);\n    next.delete(key);\n    held = next;\n  }\n  // letting go of the last step ends the shortcut; an earlier step stays done while the next is awaited\n  if (live.complete && steps[live.step].some((k) => !held.has(k))) return { ...REST, held };\n  return held === live.held ? live : { ...live, held };\n}\n\n/** Typing a letter into a field is typing, not a shortcut. */\nconst typing = (e: KeyboardEvent) => {\n  const t = e.target as HTMLElement | null;\n  return (\n    e.key.length === 1 &&\n    !e.metaKey &&\n    !e.ctrlKey &&\n    !e.altKey &&\n    !!t &&\n    (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))\n  );\n};\n\nexport interface KbdClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the cap part; no override by default. */\n  cap?: string;\n  /** Classes for the symbol part; no override by default. */\n  symbol?: string;\n  /** Classes for the spoken part; no override by default. */\n  spoken?: string;\n  /** The word between the steps of a sequence. */\n  then?: string;\n}\n\nexport interface KbdProps extends HTMLAttributes<HTMLElement> {\n  /** Keys in order; held together they make a combination: `[\"mod\", \"K\"]` renders ⌘K on Mac and Ctrl K elsewhere.\n   *  `\"then\"` between keys makes a sequence: `[\"g\", \"then\", \"a\"]` is G, then A. */\n  keys?: string[];\n  /** The caps answer the keyboard: each presses down while the viewer holds its key, and the combination lights when\n   *  all are held; in a sequence each finished step stays lit while the next is awaited (1.5 s). For teaching a\n   *  shortcut where it is shown; it listens only while mounted with `live`. */\n  live?: boolean;\n  /** Platform used to name modifier keys; defaults to the viewer's detected platform. */\n  platform?: \"mac\" | \"other\";\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: KbdClassNames;\n}\n\n/**\n * Keyboard keys as keycaps, for combinations (⌘K) and sequences (G then A). `mod` resolves to ⌘ or Ctrl for the\n * viewer's platform; symbols are spoken as words for screen readers. Pass `children` for a single literal key.\n */\nexport function Kbd({\n  keys,\n  live = false,\n  platform,\n  size: sizeProp,\n  className,\n  classNames,\n  children,\n  ref,\n  ...props\n}: KbdProps) {\n  const i18n = useMessages(\"kbd\", englishMessages);\n\n  const scopeDefaults = useComponentDefaults<KbdDefaults>(\"kbd\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const detectedMac = useApplePlatform();\n  const mac = platform === undefined ? detectedMac : platform === \"mac\";\n  const map = mac ? MAP_MAC(i18n) : MAP_PC(i18n);\n  const spoken = SPOKEN(i18n);\n  // the steps: one for a combination, one per \"then\" for a sequence (a stray \"then\" at either end is no step)\n  const parts: string[][] = [[]];\n  for (const k of keys ?? []) {\n    if (k.toLowerCase() === THEN) parts.push([]);\n    else parts[parts.length - 1].push(k);\n  }\n  const steps = parts.filter((s) => s.length > 0);\n  // live: which of the caps' keys the viewer holds now, and how far through the sequence they are\n  const [state, dispatch] = useReducer(track, REST);\n  const wanted = live && keys ? steps.map((s) => s.map((k) => eventKey(k, mac)).join(\"\\u0000\")).join(\"\\u0001\") : \"\";\n  useEffect(() => {\n    if (!wanted) return;\n    const events = wanted.split(\"\\u0001\").map((s) => s.split(\"\\u0000\"));\n    const press = (e: KeyboardEvent) => {\n      if (e.repeat || typing(e)) return;\n      dispatch({ type: \"down\", key: e.key.toLowerCase(), steps: events });\n    };\n    const release = (e: KeyboardEvent) => dispatch({ type: \"up\", key: e.key.toLowerCase(), steps: events });\n    const clear = () => dispatch({ type: \"reset\" });\n    window.addEventListener(\"keydown\", press);\n    window.addEventListener(\"keyup\", release);\n    window.addEventListener(\"blur\", clear);\n    return () => {\n      window.removeEventListener(\"keydown\", press);\n      window.removeEventListener(\"keyup\", release);\n      window.removeEventListener(\"blur\", clear);\n      dispatch({ type: \"reset\" });\n    };\n  }, [wanted]);\n  // a sequence waits for its next step a moment, then starts over\n  const waiting = state.step > 0 && !state.complete;\n  useEffect(() => {\n    if (!waiting) return;\n    const t = window.setTimeout(() => dispatch({ type: \"reset\" }), SEQUENCE_MS);\n    return () => window.clearTimeout(t);\n  }, [waiting, state.done]);\n  const label = (k: string) => map[k.toLowerCase()] ?? (k.length === 1 ? k.toUpperCase() : k);\n  const cap = cn(styles.cap, \"font-sans\", size !== \"md\" && styles[size], classNames?.cap);\n  const face = (k: ReactNode) =>\n    // a label on a plain span is not read; the symbol is hidden and its word is spoken instead\n    typeof k === \"string\" && spoken[k] ? (\n      <>\n        <span data-slot=\"kbd-symbol\" className={classNames?.symbol} aria-hidden>\n          {k}\n        </span>\n        <span data-slot=\"kbd-spoken\" className={cn(\"sr-only\", classNames?.spoken)}>\n          {spoken[k]}\n        </span>\n      </>\n    ) : (\n      k\n    );\n  return (\n    <kbd\n      ref={ref}\n      data-slot=\"kbd\"\n      data-complete={state.complete || undefined}\n      className={cn(styles.keys, classNames?.root, className)}\n      {...props}\n    >\n      {keys ? (\n        steps.map((step, s) => (\n          <Fragment key={s}>\n            {s > 0 && (\n              <span\n                // a new wait restarts the line under it\n                key={waiting && s === state.step ? `then-${state.done}` : \"then\"}\n                data-slot=\"kbd-then\"\n                data-waiting={(waiting && s === state.step) || undefined}\n                className={cn(styles.then, classNames?.then)}\n                style={{ \"--_wait\": `${SEQUENCE_MS}ms` } as CSSProperties}\n              >\n                {i18n(\"then\")}\n              </span>\n            )}\n            {step.map((k, i) => {\n              const event = eventKey(k, mac);\n              return (\n                <span\n                  data-slot=\"kbd-cap\"\n                  key={i}\n                  data-pressed={(live && s === state.step && state.held.has(event)) || undefined}\n                  data-done={(live && s < state.step) || undefined}\n                  className={cap}\n                >\n                  {face(label(k))}\n                </span>\n              );\n            })}\n          </Fragment>\n        ))\n      ) : (\n        <span data-slot=\"kbd-cap\" className={cap}>\n          {face(children)}\n        </span>\n      )}\n    </kbd>\n  );\n}\n\nexport default Kbd;\n"},{"path":"beamline/components/kbd/messages.ts","type":"registry:file","target":"src/ui/beamline/components/kbd/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  then: \"then\",\n  escapeShort: \"Esc\",\n  space: \"Space\",\n  controlShort: \"Ctrl\",\n  alt: \"Alt\",\n  shift: \"Shift\",\n  enter: \"Enter\",\n  backspace: \"Backspace\",\n  tab: \"Tab\",\n  command: \"Command\",\n  control: \"Control\",\n  option: \"Option\",\n  up: \"Up\",\n  down: \"Down\",\n  left: \"Left\",\n  right: \"Right\",\n} satisfies MessageDefinitions;\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/search-field/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/search-field/defaults.ts","content":"/** Pure allowed presentation defaults for SearchField; no component or runtime imports. */\nexport interface SearchFieldDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/search-field/messages.ts","type":"registry:file","target":"src/ui/beamline/components/search-field/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  clearSearch: \"Clear search\",\n  searching: \"Searching\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/search-field/search-field.tsx","type":"registry:file","target":"src/ui/beamline/components/search-field/search-field.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SearchFieldDefaults } from \"./defaults\";\nexport type { SearchFieldDefaults } from \"./defaults\";\n\nimport { useEffect, useRef, useState, type InputHTMLAttributes, type ReactNode, type Ref } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { IconAction } from \"../_shared/icon-action\";\nimport {\n  Field,\n  FieldFrame,\n  fieldControlClass,\n  useFieldIds,\n  type FieldClassNames,\n  type FieldSize,\n} from \"../_shared/field\";\nimport { Glow } from \"../glow/glow\";\nimport { Kbd } from \"../kbd/kbd\";\nimport { Spinner } from \"../spinner/spinner\";\n\nexport interface SearchFieldProps extends Omit<\n  InputHTMLAttributes<HTMLInputElement>,\n  \"type\" | \"size\" | \"value\" | \"defaultValue\"\n> {\n  /** Accessible name; hidden by default (the magnifier and placeholder say what the field is). */\n  label: ReactNode;\n  /** Show the label above the field. */\n  showLabel?: boolean;\n  /** Controlled search text; omit to use defaultValue. */\n  value?: string;\n  /** Initial search text when uncontrolled. Defaults to an empty string. */\n  defaultValue?: string;\n  /** Reports search text after an edit; no callback by default. */\n  onValueChange?: (value: string) => void;\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: FieldSize;\n  /** A shortcut hint shown while the field is empty, e.g. \"/\" or \"⌘K\". A one-key shortcut (\"/\") is the field's own: pressed\n   *  anywhere but in a text field, it focuses the search. A chord (\"⌘K\") is shown only; the app binds it. */\n  shortcut?: string;\n  /** A band of light rides the border: \"focus\" while focused, true while results are on their way. */\n  beam?: boolean | \"focus\";\n  /** Results are on their way: the magnifier turns into a spinner (aria-busy); typing goes on. */\n  loading?: boolean;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for documented field parts; root style does not replace native behavior. */\n  classNames?: FieldClassNames & Partial<Record<\"input\" | \"clear\", string>>;\n  /** Receives the native input; no ref by default. */\n  ref?: Ref<HTMLInputElement>;\n}\n\n/** The search entry point: magnifier, clear button, Escape to clear, an optional shortcut hint and a live beam. */\nexport function SearchField({\n  label,\n  showLabel = false,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onChange,\n  size: sizeProp,\n  shortcut,\n  beam,\n  loading,\n  id,\n  className,\n  classNames,\n  style,\n  disabled,\n  onKeyDown,\n  onFocus,\n  onBlur,\n  ref,\n  ...props\n}: SearchFieldProps) {\n  const i18n = useMessages(\"search-field\", englishMessages);\n\n  const scopeDefaults = useComponentDefaults<SearchFieldDefaults>(\"search-field\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const { controlId, aria } = useFieldIds(id);\n  const [text, set] = useControllable(value, defaultValue, onValueChange);\n  const [focused, setFocused] = useState(false);\n  const input = useRef<HTMLInputElement | null>(null);\n  const attach = useComposedRef(input, ref);\n  const frameRef = useRef<HTMLDivElement>(null);\n  // the shortcut hint is the first thing to go when the field is narrow: the words typed matter more\n  const roomy = useWidthAs(frameRef, (w) => w >= 200, true);\n  // a one-key shortcut works where it is shown: pressed outside a text field, it brings the person here\n  useEffect(() => {\n    if (!shortcut || [...shortcut].length !== 1 || disabled) return;\n    const down = (e: KeyboardEvent) => {\n      if (e.key !== shortcut || e.metaKey || e.ctrlKey || e.altKey || e.defaultPrevented) return;\n      const at = e.target instanceof HTMLElement ? e.target : null;\n      if (at?.isContentEditable || at?.closest(\"input, textarea, select\")) return;\n      e.preventDefault();\n      input.current?.focus();\n    };\n    window.addEventListener(\"keydown\", down);\n    return () => window.removeEventListener(\"keydown\", down);\n  }, [shortcut, disabled]);\n  // the magnifier comes back scaled in after a search, never on the first paint\n  const [searched, setSearched] = useState(false);\n  if (loading && !searched) setSearched(true);\n  const trailing = text ? (\n    <IconAction\n      data-slot=\"search-clear\"\n      aria-label={i18n(\"clearSearch\")}\n      disabled={disabled}\n      onClick={() => {\n        set(\"\");\n        input.current?.focus();\n      }}\n      className={cn(\"-me-1 animate-enter-scale\", classNames?.clear)}\n    >\n      <X className=\"size-3.5\" />\n    </IconAction>\n  ) : shortcut && !focused && roomy ? (\n    <Kbd className=\"animate-enter-fade\">{shortcut}</Kbd>\n  ) : null;\n  const frame = (\n    <FieldFrame\n      ref={frameRef}\n      classNames={classNames}\n      size={size}\n      disabled={disabled}\n      leading={\n        // one place for what the field is doing: the magnifier, or the ring while results are on their way\n        loading ? (\n          <Spinner key=\"busy\" size=\"sm\" label={i18n(\"searching\")} className=\"animate-enter-scale\" />\n        ) : (\n          <Search key=\"rest\" className={searched ? \"animate-enter-scale\" : undefined} />\n        )\n      }\n      trailing={trailing}\n      // nesting input (O1 §14.1): the shortcut keycap (22px) rounds concentric with the field — a pill in a pill field\n      className=\"[--pui-nest-inset:calc((var(--_field-height)-1.375rem)/2)] [--pui-nest-radius:var(--pui-field-radius,var(--pui-radius-control))]\"\n    >\n      {\" \"}\n      <input\n        ref={attach}\n        data-slot=\"search-field\"\n        {...aria}\n        type=\"search\"\n        role=\"searchbox\"\n        aria-busy={loading || undefined}\n        value={text}\n        disabled={disabled}\n        autoComplete=\"off\"\n        spellCheck={false}\n        className={cn(fieldControlClass, \"[&::-webkit-search-cancel-button]:hidden\", classNames?.input)}\n        onChange={(e) => {\n          set(e.target.value);\n          onChange?.(e);\n        }}\n        onKeyDown={(e) => {\n          // Escape clears; on an empty field it lets go of the field\n          if (e.key === \"Escape\" && text) {\n            e.preventDefault();\n            set(\"\");\n          } else if (e.key === \"Escape\") e.currentTarget.blur();\n          onKeyDown?.(e);\n        }}\n        onFocus={(e) => {\n          setFocused(true);\n          onFocus?.(e);\n        }}\n        onBlur={(e) => {\n          setFocused(false);\n          onBlur?.(e);\n        }}\n        {...props}\n      />\n    </FieldFrame>\n  );\n  const active = beam === \"focus\" ? focused : !!beam;\n  return (\n    <Field\n      label={label}\n      hideLabel={!showLabel}\n      controlId={controlId}\n      className={className}\n      classNames={classNames}\n      style={style}\n    >\n      {/* the wrapper stays while a beam is wanted at all, even off: swapping it in and out would remount the input and drop focus */}\n      {beam !== undefined ? (\n        <Glow effect=\"ring\" active={active} radius=\"var(--pui-radius-control)\" intensity={0.9}>\n          {frame}\n        </Glow>\n      ) : (\n        frame\n      )}\n    </Field>\n  );\n}\n\nexport default SearchField;\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/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-apple-platform.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-apple-platform.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { isApplePlatform } from \"../lib/platform\";\n\nconst never = () => () => {};\n\n/** isApplePlatform() for rendering: false on the server and in the first hydrated paint, the real answer after. */\nexport function useApplePlatform() {\n  return useSyncExternalStore(never, isApplePlatform, () => false);\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-content-width.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-content-width.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\nconst same = (width: number) => width;\n\n/**\n * The width of an element's content box, kept current with a ResizeObserver. It is first read in a layout effect, so\n * a layout chosen by width (a narrow mode, a chart's scale) is right in the first frame instead of jumping a frame\n * later. `fallback` stands in on the server and until the element exists; `key` re-measures when the element is\n * replaced (one that exists only in one view).\n */\nexport function useContentWidth(ref: RefObject<HTMLElement | null>, fallback: number, key?: unknown): number {\n  return useWidthAs(ref, same, fallback, key);\n}\n\n/**\n * The same width, kept as what a part needs from it (`(w) => w < 640` for a narrow mode): the part renders again only\n * when that changes, not at every pixel.\n */\nexport function useWidthAs<T>(\n  ref: RefObject<HTMLElement | null>,\n  read: (width: number) => T,\n  fallback: T,\n  key?: unknown,\n): T {\n  const [value, setValue] = useState(fallback);\n  const latest = useRef(read);\n  latest.current = read;\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    // the computed width is fractional and ignores transforms, like the observer's content box\n    const s = getComputedStyle(el);\n    const edges = [\"paddingLeft\", \"paddingRight\", \"borderLeftWidth\", \"borderRightWidth\"] as const;\n    const box = parseFloat(s.width);\n    const outer = Number.isFinite(box) ? box : el.offsetWidth;\n    const inner =\n      Number.isFinite(box) && s.boxSizing !== \"border-box\" ? 0 : edges.reduce((w, k) => w + parseFloat(s[k]), 0);\n    setValue(latest.current(outer - inner));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(latest.current(e.contentRect.width)));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key]);\n  return value;\n}\n\n/**\n * The height of an element's content box, kept current like `useContentWidth`: for parts that lay out at whatever\n * height their parent gives them (a chart, a map or a book that fills its panel). Read in a layout effect, so the first\n * frame is right; with `enabled` false nothing is measured and the fallback stands.\n */\nexport function useContentHeight(\n  ref: RefObject<HTMLElement | null>,\n  fallback: number,\n  key?: unknown,\n  enabled = true,\n): number {\n  const [value, setValue] = useState(fallback);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!enabled || !el) return;\n    const s = getComputedStyle(el);\n    setValue(el.clientHeight - (parseFloat(s.paddingTop) || 0) - (parseFloat(s.paddingBottom) || 0));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(e.contentRect.height));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key, enabled]);\n  return enabled ? value : fallback;\n}\n"},{"path":"beamline/hooks/use-controllable.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-controllable.ts","content":"\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\n/**\n * One value in both forms (rules/CONVENTIONS.md): the prop while it is passed, otherwise the element's own copy that\n * starts at `initial` (a value, or a function that makes it once). The setter takes a value or an updater; an updater\n * reads the latest value even twice in one event, and every change reaches `onChange`, controlled or not.\n * `Next` is what a change can carry when it is narrower than the state: a range that starts empty (null) but is only\n * ever changed to a range.\n */\nexport function useControllable<T, Next extends T = T>(\n  value: T | undefined,\n  initial: T | (() => T),\n  onChange?: (next: Next) => void,\n) {\n  const [own, setOwn] = useState<T>(initial);\n  const current = value !== undefined ? value : own;\n  const latest = useRef(current);\n  latest.current = current;\n  const set = useCallback(\n    (next: Next | ((was: T) => Next)) => {\n      const resolved = typeof next === \"function\" ? (next as (was: T) => Next)(latest.current) : next;\n      latest.current = resolved;\n      if (value === undefined) setOwn(resolved);\n      onChange?.(resolved);\n    },\n    [value, onChange],\n  );\n  return [current, set] as const;\n}\n"},{"path":"beamline/hooks/use-deprecated-prop.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-deprecated-prop.ts","content":"\"use client\";\n\nimport { useEffect } from \"react\";\n\n// Bundlers replace NODE_ENV for browser builds; this declaration emits no Node dependency.\ndeclare const process: { env: { NODE_ENV?: string } };\n\nconst warned = new Set<string>();\n\n/** Report a legacy prop once after mount in development; production and server renders stay silent. */\nexport function useDeprecatedProp(component: string, prop: string, replacement: string, supplied: boolean) {\n  useEffect(() => {\n    if (process.env.NODE_ENV !== \"development\" || !supplied) return;\n    const key = `${component}.${prop}`;\n    if (warned.has(key)) return;\n    warned.add(key);\n    console.warn(`[Beamline] ${key} is deprecated. Use ${replacement} instead; the legacy prop still works.`);\n  }, [component, prop, replacement, supplied]);\n}\n"},{"path":"beamline/hooks/use-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/platform.ts","type":"registry:file","target":"src/ui/beamline/lib/platform.ts","content":"/** Whether the person is on an Apple platform, where `mod` is ⌘ (Mac, iPhone, iPad); false where there is no browser. */\nexport const isApplePlatform = () =>\n  typeof navigator !== \"undefined\" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);\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":"Search field is in src/ui/beamline/components/search-field/search-field.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/search-field"}}