{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"notification-preferences","type":"registry:block","title":"Notification preferences","description":"What reaches a person and where: events grouped by area down the side, channels (email, push, Slack, in-app) across the top, a switch per pair that saves on its own and steps back if refused, a column switch for a whole channel, channels that still need connecting, alerts that cannot be turned off, a digest, quiet hours and a pause for everything. In a narrow room each event carries its channels as a row of toggles.","categories":["blocks"],"dependencies":["@radix-ui/react-direction@1.1.4","@radix-ui/react-dropdown-menu@2.1.24","@radix-ui/react-label@2.1.15","@radix-ui/react-popover@1.1.23","@radix-ui/react-select@2.3.7","@radix-ui/react-slot@1.3.3","@radix-ui/react-switch@1.3.7","@radix-ui/react-toggle-group@1.1.19","@radix-ui/react-tooltip@1.2.16","clsx@^2.1.1","lucide-react@^1.48.0","motion@^13.4.4","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/blocks/notification-preferences/messages.ts","type":"registry:file","target":"src/ui/beamline/blocks/notification-preferences/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  byDidNotChange: \"{label} by {value} did not change\",\n  didNotChange: \"{label} did not change\",\n  retry: \"Retry\",\n  didNotChange2: \"{what} did not change\",\n  notPaused: \"Not paused\",\n  stillPaused: \"Still paused\",\n  notifications: \"Notifications\",\n  chooseWhatReachesYouAndWhereChanges: \"Choose what reaches you, and where. Changes save as you make them.\",\n  saved: \"Saved {value}\",\n  pauseAll: \"Pause all\",\n  for1Hour: \"For 1 hour\",\n  untilTomorrow900: \"Until tomorrow, 9:00\",\n  forAWeek: \"For a week\",\n  yourNotificationSettingsCouldNotLoad: \"Your notification settings could not load\",\n  loadingNotificationSettings: \"Loading notification settings\",\n  pausedUntil: \"Paused until {value}\",\n  resumeNow: \"Resume now\",\n  nothingIsSentMeanwhileExceptAlertsThat: \"Nothing is sent meanwhile, except alerts that always go.\",\n  channelsFor: \"Channels for {label}\",\n  by: \"{label} by {value}\",\n  notificationsByChannel: \"Notifications by channel\",\n  event: \"Event\",\n  connect: \"Connect\",\n  notConnected: \"Not connected\",\n  countOn: \"{count} of {total}\",\n  alwaysGoesBy: \"{label} always goes by {value}\",\n  byAlwaysOn: \"{label} by {value}: always on\",\n  always: \"Always\",\n  digest: \"Digest\",\n  emailDigest: \"Email digest\",\n  off: \"Off\",\n  daily: \"Daily\",\n  weekly: \"Weekly\",\n  quietHours: \"Quiet hours\",\n  from: \"From\",\n  to: \"To\",\n  daysWithQuietHours: \"Days with quiet hours\",\n  mutePushAndSlackAtNightOr: \"Mute push and Slack at night or on weekends. Email waits for the morning.\",\n  tryAgain: \"Try again.\",\n  mon: \"Mon\",\n  tue: \"Tue\",\n  wed: \"Wed\",\n  thu: \"Thu\",\n  fri: \"Fri\",\n  sat: \"Sat\",\n  sun: \"Sun\",\n  eachOneAsItHappens: \"Each one as it happens.\",\n  oneEmailEachMorningAt800: \"One email each morning at 8:00.\",\n  oneEmailOnMondayMornings: \"One email on Monday mornings.\",\n  someEvents: \"some events\",\n  everyEvent: \"every event\",\n  noEvents: \"no events\",\n  theDigest: \"The digest\",\n  alwaysSent: \"{label}: always sent\",\n  notConnected2: \"{label}: not connected\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/blocks/notification-preferences/notification-preferences.tsx","type":"registry:file","target":"src/ui/beamline/blocks/notification-preferences/notification-preferences.tsx","content":"\"use client\";\nimport { type Translator } from \"../../runtime/messages\";\nimport { useMessages, dateFormatter } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useScreenFormatters } from \"../../components/_shared/screen\";\nimport { useLocale } from \"../../runtime/localization\";\n\nimport { useRef, useState, type CSSProperties, type ReactNode, type Ref } from \"react\";\nimport { BellOff, Check, Lock, Moon, Play } from \"lucide-react\";\nimport { Alert } from \"../../components/alert/alert\";\nimport { Button } from \"../../components/button/button\";\nimport { DropdownMenu } from \"../../components/dropdown-menu/dropdown-menu\";\nimport { SegmentedControl } from \"../../components/segmented-control/segmented-control\";\nimport { Switch } from \"../../components/switch/switch\";\nimport { TimePicker } from \"../../components/time-picker/time-picker\";\nimport { ToastStack, ToastStackProvider, useToastStack } from \"../../components/toast/toast\";\nimport { ToggleGroup, ToggleGroupItem } from \"../../components/toggle-group/toggle-group\";\nimport { Tooltip } from \"../../components/tooltip/tooltip\";\nimport { ScreenHeader, ScreenLoading, ScreenProblem, errorText, useControllable } from \"../../components/_shared/screen\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\nimport { useNow } from \"../../lib/now\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface NotifyChannel {\n  /** Stable identifier used to match this record across updates. Required. */\n  id: string;\n  /** Text displayed for this item. Required. */\n  label: string;\n  /** Icon element displayed with this item. Omitted by default. */\n  icon?: ReactNode;\n  /** Where it goes: an address, a room (\"#deals\"). */\n  detail?: string;\n  /** false: not set up yet; shows Connect and its switches wait. */\n  connected?: boolean;\n}\nexport interface NotifyEvent {\n  /** Stable identifier used to match this record across updates. Required. */\n  id: string;\n  /** Text displayed for this item. Required. */\n  label: string;\n  /** Group heading under which this item appears. Required. */\n  group: string;\n  /** Supporting content describing this item. Omitted by default. */\n  description?: string;\n  /** Channels this event always goes to (security alerts by email): locked on. */\n  required?: string[];\n}\nexport interface QuietHours {\n  /** Whether this feature is enabled. Required. */\n  enabled: boolean;\n  /** Start of quiet hours in 24-hour HH:mm notation. Required. */\n  from: string;\n  /** End of quiet hours in 24-hour HH:mm notation. Required. */\n  to: string;\n  /** 0 Monday … 6 Sunday. */\n  days: number[];\n}\nexport type NotificationPreferencesClassNames = Partial<\n  Record<\"root\" | \"header\" | \"paused\" | \"matrix\" | \"list\" | \"digest\" | \"quiet\" | \"toasts\", string>\n>;\n\nexport interface NotificationPreferencesProps {\n  /** Delivery channels offered in the preference matrix. Required. */\n  channels: NotifyChannel[];\n  /** Notification events shown as rows in the preference matrix. Required. */\n  events: NotifyEvent[];\n  /** The channels each event goes to, controlled: pass with onValueChange. */\n  value?: Record<string, string[]>;\n  /** Initial channel ids enabled for each event when uncontrolled. Defaults to an empty object. */\n  defaultValue?: Record<string, string[]>;\n  /** Reports channel ids enabled for each event after an edit; no callback by default. */\n  onValueChange?: (value: Record<string, string[]>) => void;\n  /** Saves one change at once (event null: the whole channel); a rejection steps it back. */\n  onToggle: (event: string | null, channel: string, on: boolean) => Promise<void>;\n  /** Initial digest schedule saved through onDigestChange; omit to hide the digest section. */\n  digest?: \"off\" | \"daily\" | \"weekly\";\n  /** Reports digest schedule after a change; no callback by default. */\n  onDigestChange?: (digest: \"off\" | \"daily\" | \"weekly\") => Promise<void>;\n  /** Initial quiet-hours schedule saved through onQuietHoursChange; omit to hide that section. */\n  quietHours?: QuietHours;\n  /** Reports quiet hours after a change; no callback by default. */\n  onQuietHoursChange?: (quietHours: QuietHours) => Promise<void>;\n  /** Everything paused until this time (ms); null when not paused. */\n  pausedUntil?: number | null;\n  /** Pauses notifications until the chosen time; no handler by default. */\n  onPause?: (until: number | null) => Promise<void>;\n  /** Starts connecting the specified notification channel; no handler by default. */\n  onConnectChannel?: (id: string) => void;\n  /** Shows the loading state while data or an operation is pending. Defaults to false. */\n  loading?: boolean;\n  /** Error message shown in place of the usual supporting text. Defaults to no error message. */\n  error?: ReactNode;\n  /** Runs the retry action for the failed operation; no handler by default. */\n  onRetry?: () => void | Promise<unknown>;\n  /** Heading level for the page title. Defaults to 1. */\n  level?: 1 | 2;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: NotificationPreferencesClassNames;\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}\nfunction localizedDAYS(i18n: Translator<typeof englishMessages>) {\n  return [i18n(\"mon\"), i18n(\"tue\"), i18n(\"wed\"), i18n(\"thu\"), i18n(\"fri\"), i18n(\"sat\"), i18n(\"sun\")];\n}\nfunction localizedDIGEST_NOTE(i18n: Translator<typeof englishMessages>) {\n  return {\n    off: i18n(\"eachOneAsItHappens\"),\n    daily: i18n(\"oneEmailEachMorningAt800\"),\n    weekly: i18n(\"oneEmailOnMondayMornings\"),\n  };\n}\nconst k = (event: string, channel: string) => `${event}\\u0000${channel}`;\n\n/**\n * What reaches a person and where: events by channel, each switch saving on its own and stepping back if refused,\n * a column switch per channel, channels to connect, locked alerts, a digest, quiet hours and a pause for everything.\n */\nexport function NotificationPreferences(props: NotificationPreferencesProps) {\n  return (\n    <ToastStackProvider>\n      <Body {...props} />\n    </ToastStackProvider>\n  );\n}\n\nfunction Body({\n  channels,\n  events,\n  value: valueProp,\n  defaultValue = {},\n  onValueChange,\n  onToggle,\n  digest: digestProp,\n  onDigestChange,\n  quietHours: quietProp,\n  onQuietHoursChange,\n  pausedUntil: pausedProp,\n  onPause,\n  onConnectChannel,\n  loading,\n  error,\n  onRetry,\n  level = 1,\n  className,\n  classNames,\n  style,\n  ref,\n}: NotificationPreferencesProps) {\n  const i18n = useMessages(\"notification-preferences\", englishMessages);\n\n  const locale = useLocale();\n  const { ago } = useScreenFormatters(locale);\n\n  const [value, setValue] = useControllable(valueProp, defaultValue, onValueChange);\n  const [busy, setBusy] = useState<string[]>([]);\n  const pending = useRef(new Set<string>());\n  const [savedAt, setSavedAt] = useState<number | null>(null);\n  const [digest, setDigest] = useState(digestProp);\n  const [quiet, setQuiet] = useState(quietProp);\n  const [paused, setPaused] = useState<number | null>(pausedProp ?? null);\n  const [pausing, setPausing] = useState(false);\n  const rootRef = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(rootRef, ref);\n  const narrow = useWidthAs(rootRef, (w) => w < 720, false);\n  const now = useNow(true, 30000);\n  const { toast } = useToastStack();\n  const isPaused = paused !== null && (now === null || paused > now);\n\n  const groups: { name: string; items: NotifyEvent[] }[] = [];\n  for (const e of events) {\n    const g = groups.find((x) => x.name === e.group);\n    if (g) g.items.push(e);\n    else groups.push({ name: e.group, items: [e] });\n  }\n  const on = (e: NotifyEvent, c: NotifyChannel) => !!e.required?.includes(c.id) || !!value[e.id]?.includes(c.id);\n  const locked = (e: NotifyEvent, c: NotifyChannel) => !!e.required?.includes(c.id);\n  const live = (c: NotifyChannel) => c.connected !== false;\n\n  const apply = (changes: [string, string, boolean][]) =>\n    setValue((was) => {\n      const out = { ...was };\n      for (const [eid, cid, next] of changes) {\n        const set = new Set(out[eid] ?? []);\n        if (next) set.add(cid);\n        else set.delete(cid);\n        out[eid] = [...set];\n      }\n      return out;\n    });\n  const toggle = async (event: NotifyEvent | null, channel: NotifyChannel, next: boolean) => {\n    const targets = event ? [event] : events.filter((e) => !locked(e, channel));\n    if (pending.current.has(k(\"*\", channel.id)) || targets.some((e) => pending.current.has(k(e.id, channel.id))))\n      return;\n    // each pair steps back to exactly what it was if the change is refused\n    const back = targets.map(\n      (e) => [e.id, channel.id, !!value[e.id]?.includes(channel.id)] as [string, string, boolean],\n    );\n    const keys = event ? [k(event.id, channel.id)] : [k(\"*\", channel.id)];\n    for (const key of keys) pending.current.add(key);\n    apply(targets.map((e) => [e.id, channel.id, next]));\n    setBusy([...pending.current]);\n    try {\n      await onToggle(event?.id ?? null, channel.id, next);\n      setSavedAt(Date.now());\n    } catch (e) {\n      apply(back);\n      toast({\n        type: \"error\",\n        title: event\n          ? i18n(\"byDidNotChange\", { label: event.label, value: channel.label.toLowerCase() })\n          : i18n(\"didNotChange\", { label: channel.label }),\n        description: errorText(e, i18n(\"tryAgain\")),\n        action: { label: i18n(\"retry\"), onClick: () => void toggle(event, channel, next) },\n      });\n    } finally {\n      for (const key of keys) pending.current.delete(key);\n      setBusy([...pending.current]);\n    }\n  };\n  const columnCount = (c: NotifyChannel) => {\n    const free = events.filter((e) => !locked(e, c));\n    return { on: free.filter((e) => on(e, c)).length, of: free.length };\n  };\n  const columnState = (c: NotifyChannel) => {\n    const { on: n, of } = columnCount(c);\n    return n === 0 ? \"off\" : n === of ? \"on\" : \"some\";\n  };\n  const save = async <T,>(\n    set: (v: T) => void,\n    was: T,\n    next: T,\n    handler: ((v: T) => Promise<void>) | undefined,\n    what: string,\n  ) => {\n    set(next);\n    try {\n      await handler?.(next);\n      setSavedAt(Date.now());\n    } catch (e) {\n      set(was);\n      toast({\n        type: \"error\",\n        title: i18n(\"didNotChange2\", { what: what }),\n        description: errorText(e, i18n(\"tryAgain\")),\n      });\n    }\n  };\n  const pause = async (until: number | null) => {\n    setPausing(true);\n    const was = paused;\n    setPaused(until);\n    try {\n      await onPause?.(until);\n      setSavedAt(Date.now());\n    } catch (e) {\n      setPaused(was);\n      toast({\n        type: \"error\",\n        title: until ? i18n(\"notPaused\") : i18n(\"stillPaused\"),\n        description: errorText(e, i18n(\"tryAgain\")),\n      });\n    } finally {\n      setPausing(false);\n    }\n  };\n  const until = (t: number) => {\n    const d = new Date(t);\n    const sameDay = now !== null && new Date(now).toDateString() === d.toDateString();\n    return dateFormatter(\n      locale,\n      sameDay ? { hour: \"numeric\", minute: \"2-digit\" } : { weekday: \"short\", hour: \"numeric\", minute: \"2-digit\" },\n    ).format(d);\n  };\n\n  return (\n    <div\n      ref={attach}\n      style={style}\n      data-slot=\"notification-preferences\"\n      data-layout={narrow ? \"list\" : \"matrix\"}\n      data-paused={isPaused || undefined}\n      className={cn(\"@container relative flex min-h-0 w-full flex-col gap-5\", classNames?.root, className)}\n    >\n      <ScreenHeader\n        level={level}\n        className={classNames?.header}\n        title={i18n(\"notifications\")}\n        description={i18n(\"chooseWhatReachesYouAndWhereChanges\")}\n        meta={\n          savedAt ? (\n            <span key={savedAt} role=\"status\" className=\"flex animate-enter-fade items-center gap-1 text-xs text-ink-3\">\n              <Check className=\"size-3.5 text-green\" />\n              {i18n(\"saved\", { value: ago(savedAt, now) })}\n            </span>\n          ) : null\n        }\n        actions={\n          onPause && !isPaused && !error && !loading ? (\n            <DropdownMenu\n              label={i18n(\"pauseAll\")}\n              variant=\"secondary\"\n              icon={<BellOff className=\"size-4\" />}\n              showLabel\n              items={[\n                { label: i18n(\"for1Hour\"), onSelect: () => void pause(Date.now() + 36e5) },\n                {\n                  label: i18n(\"untilTomorrow900\"),\n                  onSelect: () => {\n                    const t = new Date();\n                    t.setDate(t.getDate() + 1);\n                    t.setHours(9, 0, 0, 0);\n                    void pause(t.getTime());\n                  },\n                },\n                { label: i18n(\"forAWeek\"), onSelect: () => void pause(Date.now() + 7 * 864e5) },\n              ]}\n            />\n          ) : undefined\n        }\n      />\n      {error ? (\n        <ScreenProblem title={i18n(\"yourNotificationSettingsCouldNotLoad\")} error={error} onRetry={onRetry} />\n      ) : loading ? (\n        <ScreenLoading label={i18n(\"loadingNotificationSettings\")} rows={4} />\n      ) : (\n        <>\n          {isPaused && paused !== null && (\n            <Alert\n              className={classNames?.paused}\n              tone=\"info\"\n              icon={<BellOff />}\n              title={i18n(\"pausedUntil\", { value: until(paused) })}\n              action={\n                <Button size=\"sm\" loading={pausing} onClick={() => void pause(null)}>\n                  <Play className=\"size-4\" />\n                  {i18n(\"resumeNow\")}\n                </Button>\n              }\n            >\n              {i18n(\"nothingIsSentMeanwhileExceptAlertsThat\")}\n            </Alert>\n          )}\n\n          {narrow ? (\n            <div\n              data-slot=\"notification-preferences-list\"\n              className={cn(\"m-surface flex flex-col rounded-card px-4 py-2\", classNames?.list)}\n            >\n              {groups.map((g) => (\n                <section key={g.name} aria-label={g.name} className=\"py-2\">\n                  <h2 className=\"text-overline py-2 text-ink-3\">{g.name}</h2>\n                  {g.items.map((e) => (\n                    <div key={e.id} className=\"flex flex-col gap-2 border-b border-line-soft py-3 last:border-b-0\">\n                      <span className=\"flex flex-col\">\n                        <span className=\"text-sm text-ink\">{e.label}</span>\n                        {e.description && <span className=\"text-xs text-ink-3\">{e.description}</span>}\n                      </span>\n                      <ToggleGroup\n                        type=\"multiple\"\n                        variant=\"outline\"\n                        size=\"sm\"\n                        aria-label={i18n(\"channelsFor\", { label: e.label })}\n                        value={channels.filter((c) => on(e, c)).map((c) => c.id)}\n                        onValueChange={(ids: string[]) => {\n                          const c = channels.find((x) => ids.includes(x.id) !== on(e, x));\n                          if (c && live(c) && !locked(e, c)) void toggle(e, c, ids.includes(c.id));\n                        }}\n                      >\n                        {channels.map((c) => (\n                          <ToggleGroupItem\n                            key={c.id}\n                            value={c.id}\n                            disabled={\n                              !live(c) || locked(e, c) || busy.includes(k(e.id, c.id)) || busy.includes(k(\"*\", c.id))\n                            }\n                            aria-label={i18n(\"by\", { label: e.label, value: c.label.toLowerCase() })}\n                            tooltip={\n                              locked(e, c)\n                                ? i18n(\"alwaysSent\", { label: c.label })\n                                : !live(c)\n                                  ? i18n(\"notConnected2\", { label: c.label })\n                                  : c.label\n                            }\n                          >\n                            {c.icon}\n                          </ToggleGroupItem>\n                        ))}\n                      </ToggleGroup>\n                    </div>\n                  ))}\n                </section>\n              ))}\n            </div>\n          ) : (\n            <div className=\"m-surface overflow-x-auto rounded-card\">\n              <table\n                className={cn(\"w-full border-separate border-spacing-0 text-sm\", classNames?.matrix)}\n                aria-label={i18n(\"notificationsByChannel\")}\n              >\n                <thead>\n                  <tr>\n                    <th\n                      scope=\"col\"\n                      className=\"border-b border-line-soft p-4 text-start align-bottom text-xs font-normal text-ink-3\"\n                    >\n                      {i18n(\"event\")}\n                    </th>\n                    {channels.map((c) => {\n                      const state = columnState(c);\n                      return (\n                        <th\n                          key={c.id}\n                          scope=\"col\"\n                          className=\"min-w-32 border-b border-line-soft p-4 text-center align-bottom font-normal\"\n                        >\n                          <span className=\"flex flex-col items-center gap-1.5\">\n                            <span className=\"flex items-center gap-1.5 text-sm text-ink [&>svg]:size-4 [&>svg]:text-ink-3\">\n                              {c.icon}\n                              {c.label}\n                            </span>\n                            {/* every head keeps the detail line, so the titles and switches line up across */}\n                            <span className=\"max-w-36 truncate text-xs text-ink-3\" aria-hidden={!c.detail || undefined}>\n                              {c.detail ?? \" \"}\n                            </span>\n                            {live(c) ? (\n                              <Switch\n                                size=\"sm\"\n                                aria-label={`${c.label}: ${state === \"some\" ? i18n(\"someEvents\") : state === \"on\" ? i18n(\"everyEvent\") : i18n(\"noEvents\")}`}\n                                checked={state === \"on\"}\n                                disabled={busy.some((key) => key.endsWith(`\\u0000${c.id}`))}\n                                loading={busy.includes(k(\"*\", c.id))}\n                                onCheckedChange={(v) => void toggle(null, c, v)}\n                              />\n                            ) : onConnectChannel ? (\n                              <Button size=\"sm\" variant=\"secondary\" onClick={() => onConnectChannel(c.id)}>\n                                {i18n(\"connect\")}\n                              </Button>\n                            ) : (\n                              <span className=\"text-xs text-ink-3\">{i18n(\"notConnected\")}</span>\n                            )}\n                            {live(c) && (\n                              // how much of the column is on, so the head reads the channel at a glance\n                              <span aria-hidden className=\"font-numeric text-xs text-ink-3\">\n                                {i18n(\"countOn\", { count: columnCount(c).on, total: columnCount(c).of })}\n                              </span>\n                            )}\n                          </span>\n                        </th>\n                      );\n                    })}\n                  </tr>\n                </thead>\n                {groups.map((g) => (\n                  <tbody key={g.name}>\n                    <tr>\n                      <th\n                        scope=\"rowgroup\"\n                        colSpan={channels.length + 1}\n                        className=\"text-overline px-4 pt-5 pb-1 text-start font-normal text-ink-3\"\n                      >\n                        {g.name}\n                      </th>\n                    </tr>\n                    {g.items.map((e) => (\n                      <tr key={e.id} className=\"hover:bg-hover\">\n                        <th scope=\"row\" className=\"border-b border-line-soft px-4 py-3 text-start font-normal\">\n                          <span className=\"flex flex-col\">\n                            <span className=\"text-ink\">{e.label}</span>\n                            {e.description && <span className=\"text-xs text-ink-3\">{e.description}</span>}\n                          </span>\n                        </th>\n                        {channels.map((c) => (\n                          <td key={c.id} className=\"border-b border-line-soft px-4 py-3 text-center\">\n                            {locked(e, c) ? (\n                              <Tooltip\n                                content={i18n(\"alwaysGoesBy\", {\n                                  label: e.label,\n                                  value: c.label.toLowerCase(),\n                                })}\n                              >\n                                <span\n                                  tabIndex={0}\n                                  aria-label={i18n(\"byAlwaysOn\", {\n                                    label: e.label,\n                                    value: c.label.toLowerCase(),\n                                  })}\n                                  className=\"inline-flex items-center gap-1 rounded-chip text-xs text-ink-3\"\n                                >\n                                  <Lock className=\"size-3.5\" />\n                                  {i18n(\"always\")}\n                                </span>\n                              </Tooltip>\n                            ) : (\n                              <span className={cn(\"inline-flex\", !live(c) && \"opacity-40\")}>\n                                <Switch\n                                  size=\"sm\"\n                                  aria-label={i18n(\"by\", { label: e.label, value: c.label.toLowerCase() })}\n                                  disabled={!live(c) || busy.includes(k(\"*\", c.id))}\n                                  checked={on(e, c)}\n                                  loading={busy.includes(k(e.id, c.id))}\n                                  onCheckedChange={(v) => void toggle(e, c, v)}\n                                />\n                              </span>\n                            )}\n                          </td>\n                        ))}\n                      </tr>\n                    ))}\n                  </tbody>\n                ))}\n              </table>\n            </div>\n          )}\n\n          {(digest !== undefined || quiet !== undefined) && (\n            <div className=\"grid gap-4 @2xl:grid-cols-2\">\n              {digest !== undefined && (\n                <section\n                  aria-label={i18n(\"digest\")}\n                  className={cn(\"m-surface flex flex-col gap-3 rounded-card p-5\", classNames?.digest)}\n                >\n                  <h2 className=\"text-subheading text-ink\">{i18n(\"emailDigest\")}</h2>\n                  <SegmentedControl\n                    label={i18n(\"digest\")}\n                    showLabel={false}\n                    value={digest}\n                    onValueChange={(v) =>\n                      void save(setDigest, digest, v as \"off\" | \"daily\" | \"weekly\", onDigestChange, i18n(\"theDigest\"))\n                    }\n                    options={[\n                      { value: \"off\", label: i18n(\"off\") },\n                      { value: \"daily\", label: i18n(\"daily\") },\n                      { value: \"weekly\", label: i18n(\"weekly\") },\n                    ]}\n                  />\n                  <p className=\"text-sm text-ink-2\">{localizedDIGEST_NOTE(i18n)[digest]}</p>\n                </section>\n              )}\n              {quiet !== undefined && (\n                <section\n                  aria-label={i18n(\"quietHours\")}\n                  className={cn(\"m-surface flex flex-col gap-3 rounded-card p-5\", classNames?.quiet)}\n                >\n                  <div className=\"flex items-center gap-2\">\n                    <Moon className=\"size-4 text-ink-3\" aria-hidden />\n                    <h2 className=\"text-subheading me-auto text-ink\">{i18n(\"quietHours\")}</h2>\n                    <Switch\n                      aria-label={i18n(\"quietHours\")}\n                      checked={quiet.enabled}\n                      onCheckedChange={(v) =>\n                        void save(setQuiet, quiet, { ...quiet, enabled: v }, onQuietHoursChange, i18n(\"quietHours\"))\n                      }\n                    />\n                  </div>\n                  {quiet.enabled ? (\n                    <div className=\"flex animate-enter-fade flex-col gap-3\">\n                      <div className=\"grid grid-cols-2 gap-3\">\n                        <TimePicker\n                          label={i18n(\"from\")}\n                          value={quiet.from}\n                          minuteStep={15}\n                          onValueChange={(t) =>\n                            void save(setQuiet, quiet, { ...quiet, from: t }, onQuietHoursChange, i18n(\"quietHours\"))\n                          }\n                        />\n                        <TimePicker\n                          label={i18n(\"to\")}\n                          value={quiet.to}\n                          minuteStep={15}\n                          onValueChange={(t) =>\n                            void save(setQuiet, quiet, { ...quiet, to: t }, onQuietHoursChange, i18n(\"quietHours\"))\n                          }\n                        />\n                      </div>\n                      <ToggleGroup\n                        type=\"multiple\"\n                        variant=\"outline\"\n                        size=\"sm\"\n                        aria-label={i18n(\"daysWithQuietHours\")}\n                        value={quiet.days.map(String)}\n                        onValueChange={(d: string[]) =>\n                          void save(\n                            setQuiet,\n                            quiet,\n                            { ...quiet, days: d.map(Number).sort() },\n                            onQuietHoursChange,\n                            i18n(\"quietHours\"),\n                          )\n                        }\n                      >\n                        {localizedDAYS(i18n).map((d, i) => (\n                          <ToggleGroupItem key={d} value={String(i)} aria-label={d}>\n                            {d.slice(0, 2)}\n                          </ToggleGroupItem>\n                        ))}\n                      </ToggleGroup>\n                    </div>\n                  ) : (\n                    <p className=\"text-sm text-ink-2\">{i18n(\"mutePushAndSlackAtNightOr\")}</p>\n                  )}\n                </section>\n              )}\n            </div>\n          )}\n        </>\n      )}\n      <ToastStack className={classNames?.toasts} contained position=\"bottom-right\" />\n    </div>\n  );\n}\n\nexport default NotificationPreferences;\n"},{"path":"beamline/components/_shared/field.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/field.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A field: label (with an optional aside at the end of its row), the control, then one message line. The gap under\n   the control belongs to the message (--pui-field-gap), so a message that arrives or leaves takes its gap with it. */\n  /* a column, not a grid: a message (or a fold) that leaves pulls its gap away with a negative margin, which a flex\n     column honours and a grid track does not (it would stop at zero and stretch the leaving part back open) */\n  .field {\n    --pui-field-gap: var(--pui-space-1-5);\n    /* positioned: a hidden label or a count's spoken words (sr-only, absolutely placed) stay inside the field */\n    position: relative;\n    display: flex;\n    min-width: 0;\n    flex-direction: column;\n    gap: var(--pui-field-gap);\n  }\n  .labelRow {\n    display: flex;\n    min-width: 0;\n    align-items: baseline;\n    gap: var(--pui-space-2);\n  }\n  .aside {\n    margin-inline-start: auto;\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n  }\n\n  /* The message: one line at a time. A new line grows in to its own height (0fr to 1fr, no measuring) while the one it\n   replaces folds away above it, so the height glides from one to the other; a line that leaves with nothing after it\n   takes the gap with it. Arrival over --pui-motion-in, departure over --pui-motion-out; reduced motion keeps the fades. */\n  .messages {\n    display: grid;\n    margin-top: calc(var(--pui-field-gap, var(--pui-space-1-5)) * -1);\n  }\n  .line {\n    display: grid;\n    grid-template-rows: 1fr;\n    opacity: 1;\n    transition:\n      grid-template-rows var(--pui-motion-in) var(--pui-ease-default),\n      opacity var(--pui-fade-in, var(--pui-motion-in)) var(--pui-ease-default);\n  }\n  @starting-style {\n    .line {\n      grid-template-rows: 0fr;\n      opacity: 0;\n    }\n  }\n  /* the line a field is first drawn with does not grow in */\n  .line[data-initial] {\n    transition: none;\n  }\n  /* a swap is one change of size: the old line folds and the new one grows over the same duration and curve, so their\n     sum (the field's height) moves straight from one to the other, with no swell between; the old words fade first */\n  .line[data-leaving] {\n    grid-template-rows: 0fr;\n    opacity: 0;\n    transition:\n      grid-template-rows var(--pui-motion-out) var(--pui-ease-default),\n      opacity var(--pui-fade-in, var(--pui-motion-in)) var(--pui-ease-default);\n    pointer-events: none;\n  }\n  .messages:has(> .line[data-leaving]) > .line:not([data-leaving], [data-initial]) {\n    transition-duration: var(--pui-motion-out), var(--pui-fade-in, var(--pui-motion-in));\n  }\n  .clip {\n    min-height: 0;\n    overflow: clip;\n    overflow-clip-margin: var(--pui-space-1);\n  }\n  .message {\n    display: flex;\n    align-items: flex-start;\n    gap: var(--pui-space-1-5);\n    padding-top: var(--pui-field-gap, var(--pui-space-1-5));\n    font-size: var(--pui-text-xs);\n    line-height: 1.375;\n  }\n  .description {\n    display: block;\n    color: var(--pui-ink-3);\n  }\n  .error {\n    color: var(--pui-red);\n  }\n  .success {\n    color: var(--pui-green);\n  }\n  .icon {\n    flex-shrink: 0;\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n    margin-top: 1px;\n  }\n\n  /* The count in the label's row: tabular, quiet, shown with attention (.reveal) and in words near the limit. */\n  .count {\n    font-variant-numeric: tabular-nums;\n    white-space: nowrap;\n  }\n  .count[data-state=\"near\"],\n  .count[data-state=\"full\"] {\n    color: var(--pui-orange);\n  }\n  .count[data-state=\"over\"] {\n    color: var(--pui-red);\n  }\n\n  /* The status at the end of the frame: the spinner arrives scaled in from its centre, and closes into the check. */\n  .status {\n    display: inline-grid;\n    flex-shrink: 0;\n    place-items: center;\n    animation: status-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  @keyframes status-in {\n    from {\n      opacity: 0;\n      scale: calc(1 - 0.4 * var(--pui-motion-travel, 1));\n    }\n  }\n\n  /* The one frame every text-like control sits in: input, textarea, password, search, number, the select and date\n   triggers. Its paint (fill, edge, hover, focus) is the field role of the look in force (looks.css); invalid,\n   read-only and disabled are drawn here so they read the same in every look. */\n  .frame {\n    --_field-height: var(--pui-control-height-md);\n    position: relative;\n    display: flex;\n    min-width: 0; /* a grid or flex parent may make it narrower than the input's natural width */\n    align-items: center;\n    gap: var(--pui-space-2);\n    min-height: var(--_field-height);\n    padding-inline: var(--pui-space-2-5);\n    border-radius: var(--pui-field-radius, var(--pui-radius-control));\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    cursor: text;\n    transition:\n      box-shadow var(--pui-motion-out) var(--pui-ease-default),\n      background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .frame:hover:not([data-disabled]) {\n    transition-duration: 0ms;\n  }\n  .frame[data-state=\"open\"] {\n    box-shadow: var(--m-field-edge-focus);\n  }\n  .frame:focus-within {\n    transition: none;\n  }\n  .frame[data-disabled] {\n    opacity: var(--pui-opacity-disabled);\n    cursor: not-allowed;\n  }\n  .frame[data-readonly] {\n    --m-field-bg: transparent;\n  }\n  .sm {\n    --_field-height: var(--pui-control-height-sm);\n    padding-inline: var(--pui-space-2);\n  }\n  .lg {\n    --_field-height: var(--pui-control-height-lg);\n    padding-inline: var(--pui-space-3);\n    font-size: var(--pui-text-base);\n  }\n  .multiline {\n    align-items: stretch;\n    padding-block: var(--pui-space-2);\n  }\n\n  /* The control itself: borderless, it takes the frame's look. Focus shows on the frame, so the control's own ring is off\n   (the frame's ring is the visible focus indicator, which is always present when the control has focus). */\n  .control {\n    flex: 1;\n    min-width: 0;\n    height: 100%;\n    min-height: calc(var(--_field-height) - 2px);\n    border: 0;\n    outline: none;\n    background: transparent;\n    color: inherit;\n    font: inherit;\n    letter-spacing: inherit;\n    padding: 0;\n  }\n  .control:focus-visible {\n    outline: none;\n  }\n  .control::placeholder {\n    color: var(--pui-ink-3);\n    opacity: 1;\n  }\n  .control:disabled {\n    cursor: not-allowed;\n  }\n  textarea.control {\n    min-height: 0;\n    resize: none;\n    line-height: 1.5;\n    field-sizing: content;\n  }\n  .adornment {\n    display: inline-flex;\n    align-items: center;\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n  }\n  .adornment :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.frame {\n  composes: m-field nest-corner from global;\n}\n"},{"path":"beamline/components/_shared/field.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/field.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type CSSProperties,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n  type Ref,\n  type TransitionEvent,\n} from \"react\";\nimport { AlertCircle, CircleCheck } from \"lucide-react\";\nimport { Label } from \"../label/label\";\nimport { Spinner } from \"../spinner/spinner\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./field.module.css\";\n\nexport type FieldSize = \"sm\" | \"md\" | \"lg\";\nexport type FieldClassNames = Partial<\n  Record<\"root\" | \"label\" | \"frame\" | \"leading\" | \"trailing\" | \"description\" | \"error\" | \"success\" | \"aside\", string>\n>;\n\nexport interface FieldOptions {\n  /** Visible label, also the accessible name. */\n  label?: ReactNode;\n  /** Keep the label for assistive tech only (dense rows, toolbars). */\n  hideLabel?: boolean;\n  /** One line of help under the control. */\n  description?: ReactNode;\n  /** The problem and its fix; replaces the description and marks the control invalid. */\n  error?: ReactNode;\n  /** The confirmation that the value works (\"@mira is available\"): replaces the description, with a check. */\n  success?: ReactNode;\n  required?: boolean;\n  size?: FieldSize;\n}\n\n/** Ids and aria wiring for one field: the control gets these spread onto it. */\nexport function useFieldIds(\n  id?: string,\n  { description, error, success }: Pick<FieldOptions, \"description\" | \"error\" | \"success\"> = {},\n) {\n  const auto = useId();\n  const controlId = id ?? `field-${auto}`;\n  // the line under the field is one of three, and the control points at whichever is there\n  const descriptionId = (success || description) && !error ? `${controlId}-description` : undefined;\n  const errorId = error ? `${controlId}-error` : undefined;\n  return {\n    controlId,\n    descriptionId,\n    errorId,\n    aria: {\n      id: controlId,\n      \"aria-describedby\": [descriptionId, errorId].filter(Boolean).join(\" \") || undefined,\n      \"aria-invalid\": error ? true : undefined,\n      \"aria-errormessage\": errorId,\n    },\n  };\n}\n\n/** Label above (with an optional aside at the end of its row: a counter, a link), control frame, then the message. */\nexport function Field({\n  label,\n  hideLabel,\n  description,\n  error,\n  success,\n  required,\n  controlId,\n  descriptionId,\n  errorId,\n  aside,\n  className,\n  classNames,\n  style,\n  slot = \"field\",\n  requiredOnControl,\n  children,\n}: FieldOptions & {\n  controlId: string;\n  descriptionId?: string;\n  errorId?: string;\n  /** At the end of the label's row: a character count, \"Forgot password?\", \"Optional\". */\n  aside?: ReactNode;\n  className?: string;\n  classNames?: FieldClassNames;\n  style?: CSSProperties;\n  slot?: string;\n  /** The control carries `required` itself, so the label does not repeat it in the control's name. */\n  requiredOnControl?: boolean;\n  children: ReactNode;\n}) {\n  const labelNode = label != null && (\n    <Label\n      htmlFor={controlId}\n      required={required}\n      requiredOnControl={requiredOnControl}\n      className={cn(\"text-label\", hideLabel && \"sr-only\", classNames?.label)}\n    >\n      {label}\n    </Label>\n  );\n  return (\n    <div\n      data-slot={slot}\n      data-invalid={error ? \"\" : undefined}\n      style={style}\n      className={cn(styles.field, aside != null && \"reveal-host\", classNames?.root, className)}\n    >\n      {aside != null ? (\n        <div data-slot=\"field-label-row\" className={styles.labelRow}>\n          {labelNode}\n          <span data-slot=\"field-aside\" className={cn(styles.aside, classNames?.aside)}>\n            {aside}\n          </span>\n        </div>\n      ) : (\n        labelNode\n      )}\n      {children}\n      <FieldMessage\n        error={error}\n        success={success}\n        description={description}\n        errorId={errorId}\n        descriptionId={descriptionId}\n        classNames={classNames}\n      />\n    </div>\n  );\n}\n\ntype Tone = \"error\" | \"success\" | \"description\";\ninterface Line {\n  key: string;\n  tone: Tone;\n  content: ReactNode;\n}\nconst ICON = { error: AlertCircle, success: CircleCheck } as const;\n\n/**\n * The line under a field: its problem when there is one, else its confirmation, else its help. Shared by every field\n * and choice group. A change of line keeps its path: the new line grows in under the control while the old one folds\n * away above it, so what sits below glides instead of jumping, and a quick reversal turns around from where it is.\n * The leaving line is a still copy, hidden from assistive tech and inert; ids and aria point only at the line that is\n * there. It is gone when its fold ends, and by the clock as well: a line that leaves in the frame it arrived has no\n * fold to run, so no end is reported.\n */\nexport function FieldMessage({\n  error,\n  success,\n  description,\n  errorId,\n  descriptionId,\n  slot = \"field\",\n  classNames,\n}: Pick<FieldOptions, \"error\" | \"description\" | \"success\"> & {\n  errorId?: string;\n  descriptionId?: string;\n  /** The slot names' prefix: \"checkbox\" names the lines checkbox-error, checkbox-description. */\n  slot?: string;\n  classNames?: Pick<FieldClassNames, \"error\" | \"description\" | \"success\">;\n}) {\n  const tone: Tone | null = error ? \"error\" : success ? \"success\" : description ? \"description\" : null;\n  const content = error || success || description;\n  // a new text is a new line (one problem replacing another also folds and grows); rich content changes in place\n  const text = typeof content === \"string\" || typeof content === \"number\" ? String(content) : \"\";\n  const current: Line | null = tone ? { key: `${tone}\\u0000${text}`, tone, content } : null;\n  const [last, setLast] = useState<Line | null>(current);\n  const [leaving, setLeaving] = useState<Line | null>(null);\n  // the line a field is drawn with is simply there; only a change of line moves\n  const [changed, setChanged] = useState(false);\n  if ((last?.key ?? null) !== (current?.key ?? null)) {\n    // the line that was there leaves (one at a time: a line already leaving is gone at once)\n    setLeaving(last && last.key !== current?.key ? last : null);\n    setLast(current);\n    setChanged(true);\n  }\n  const box = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    if (!leaving) return;\n    // the clock is the leaving line's own longest transition, read once as it starts to leave (the fold's end event\n    // usually comes first), so a field resolves no motion tokens of its own\n    const el = box.current?.querySelector<HTMLElement>(\"[data-leaving]\");\n    const longest = el\n      ? Math.max(\n          0,\n          ...getComputedStyle(el)\n            .transitionDuration.split(\",\")\n            .map((t) => parseFloat(t) * (t.trim().endsWith(\"ms\") ? 1 : 1000) || 0),\n        )\n      : 0;\n    const id = window.setTimeout(() => setLeaving((l) => (l === leaving ? null : l)), longest + 250);\n    return () => window.clearTimeout(id);\n  }, [leaving]);\n  if (!current && !leaving) return null;\n  const shown = leaving && leaving.key === current?.key ? null : leaving;\n  const ids = { error: errorId, success: descriptionId, description: descriptionId };\n  const line = (l: Line, gone: boolean) => {\n    const Icon = l.tone === \"description\" ? null : ICON[l.tone];\n    return (\n      <div\n        key={l.key}\n        data-slot=\"field-message-line\"\n        data-leaving={gone ? \"\" : undefined}\n        data-initial={changed ? undefined : \"\"}\n        aria-hidden={gone || undefined}\n        inert={gone || undefined}\n        className={styles.line}\n        onTransitionEnd={\n          gone\n            ? (e: TransitionEvent<HTMLDivElement>) => {\n                if (e.target === e.currentTarget && e.propertyName === \"grid-template-rows\") setLeaving(null);\n              }\n            : undefined\n        }\n      >\n        <div className={styles.clip}>\n          <p\n            id={gone ? undefined : ids[l.tone]}\n            data-slot={`${slot}-${l.tone}`}\n            className={cn(styles.message, styles[l.tone], classNames?.[l.tone])}\n          >\n            {Icon && <Icon aria-hidden className={styles.icon} />}\n            {l.content}\n          </p>\n        </div>\n      </div>\n    );\n  };\n  return (\n    <div ref={box} data-slot=\"field-message\" className={styles.messages}>\n      {shown && line(shown, true)}\n      {current && line(current, false)}\n    </div>\n  );\n}\n\n/** What the end of a text field says about its work: a spinner while a value is checked, a drawn check when it works. */\nexport type FieldStatus = \"busy\" | \"success\";\n\n/** The frame a text-like control sits in; clicking anywhere on it focuses the control. */\nexport function FieldFrame({\n  size = \"md\",\n  invalid,\n  disabled,\n  readOnly,\n  multiline,\n  status,\n  statusLabel: statusLabelProp,\n  leading,\n  trailing,\n  className,\n  classNames,\n  children,\n  onPointerDown,\n  ref,\n}: {\n  size?: FieldSize;\n  invalid?: boolean;\n  disabled?: boolean;\n  readOnly?: boolean;\n  multiline?: boolean;\n  /** busy: a spinner at the end while the value is checked or saved; success: the ring closes into a check. */\n  status?: FieldStatus;\n  /** What the spinner says while busy (\"Checking\", \"Saving\"). */\n  statusLabel?: string;\n  leading?: ReactNode;\n  trailing?: ReactNode;\n  className?: string;\n  classNames?: FieldClassNames;\n  children: ReactNode;\n  onPointerDown?: (e: ReactPointerEvent<HTMLDivElement>) => void;\n  /** The frame itself, for a control that lays out by the frame's width (useWidthAs). */\n  ref?: Ref<HTMLDivElement>;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const statusLabel = statusLabelProp === undefined ? i18n(\"statusLabel\") : statusLabelProp;\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"field-control\"\n      data-invalid={invalid ? \"\" : undefined}\n      data-disabled={disabled ? \"\" : undefined}\n      data-readonly={readOnly ? \"\" : undefined}\n      data-status={status}\n      className={cn(styles.frame, styles[size], multiline && styles.multiline, classNames?.frame, className)}\n      onPointerDown={(e) => {\n        onPointerDown?.(e);\n        // a click on the frame's padding or an adornment that is not itself interactive focuses the control\n        const t = e.target as HTMLElement;\n        if (t === e.currentTarget || (t.closest(\"[data-adornment]\") && !t.closest(\"button, a, input, [role=button]\"))) {\n          e.preventDefault();\n          e.currentTarget.querySelector<HTMLElement>(\"input, textarea\")?.focus();\n        }\n      }}\n    >\n      {leading && (\n        <span\n          data-slot=\"field-adornment\"\n          data-adornment=\"leading\"\n          className={cn(styles.adornment, classNames?.leading)}\n        >\n          {leading}\n        </span>\n      )}\n      {children}\n      {status && (\n        // one ring for both: it spins while the value is checked and closes into the check when it works\n        <span data-slot=\"field-status\" data-adornment=\"status\" className={styles.status}>\n          <Spinner size=\"sm\" state={status === \"busy\" ? \"loading\" : \"success\"} label={statusLabel} />\n        </span>\n      )}\n      {trailing && (\n        <span\n          data-slot=\"field-adornment\"\n          data-adornment=\"trailing\"\n          className={cn(styles.adornment, classNames?.trailing)}\n        >\n          {trailing}\n        </span>\n      )}\n    </div>\n  );\n}\n\n/**\n * A character count for the label's row (Field `aside`): quiet (\"58/140\") and shown while the field has attention,\n * then always shown and in words as the limit nears (\"12 left\", \"Limit reached\", \"3 over\"), so it never competes with\n * the label at rest. Assistive tech hears it only when it crosses into those words, not at every key.\n */\nexport function FieldCount({\n  length,\n  max,\n  near: nearAt,\n  slot = \"field-count\",\n  className,\n}: {\n  length: number;\n  max: number;\n  /** How many left turns it into words: by default 10 % of the limit, at least 5 (characters); 1 suits a few tags. */\n  near?: number;\n  slot?: string;\n  className?: string;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const left = max - length;\n  const near = left <= (nearAt ?? Math.max(5, Math.ceil(max * 0.1)));\n  const state = left < 0 ? \"over\" : left === 0 ? \"full\" : near ? \"near\" : \"room\";\n  const words =\n    state === \"over\"\n      ? i18n(\"fieldOver\", { count: -left })\n      : state === \"full\"\n        ? i18n(\"fieldFull\")\n        : i18n(\"fieldLeft\", { count: left });\n  return (\n    <span\n      data-slot={slot}\n      data-state={state}\n      data-shown={state === \"room\" ? undefined : \"\"}\n      className={cn(\"reveal\", styles.count, className)}\n    >\n      <span aria-hidden>{state === \"room\" ? `${i18n.number(length)}/${i18n.number(max)}` : words}</span>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {state === \"room\" ? \"\" : words}\n      </span>\n    </span>\n  );\n}\n\n/** The class a native input or textarea needs inside a FieldFrame. */\nexport const fieldControlClass = styles.control;\n\n/** What a choice group (segmented control, radio cards) spreads onto its own root when it is a form field. */\nexport interface ChoiceFieldAria {\n  \"aria-labelledby\": string;\n  \"aria-describedby\"?: string;\n  \"aria-invalid\"?: true;\n}\n\n/**\n * Whether a choice control (segmented control, radio cards, chip group) is laid out as a form field, its label shown\n * above like every field's: when asked (`field`), when it has a description, error or required mark, and whenever its\n * label is not hidden. A view switch or a quick filter passes `hideLabel`: its label stays for assistive tech only.\n * (Trials t16 and t19: agents wrote <SegmentedControl label=\"Catering needed\" …> and the question never showed.)\n */\nexport function choiceIsField(\n  field: boolean | undefined,\n  label: ReactNode,\n  hideLabel: boolean | undefined,\n  marked: boolean,\n) {\n  return field ?? (marked || (label != null && label !== \"\" && !hideLabel));\n}\n\n/**\n * A choice group as a form field: the visible label above (with the required marker), the group, then its help or its\n * problem — the same frame and message every text field has. Groups that name themselves only for assistive tech\n * (a view switch in a toolbar) do not use it.\n */\nexport function ChoiceField({\n  slot,\n  label,\n  hideLabel,\n  required,\n  description,\n  error,\n  className,\n  style,\n  classNames,\n  requiredOnControl,\n  children,\n}: Omit<FieldOptions, \"size\" | \"success\"> & {\n  slot: string;\n  /** The group carries `aria-required` itself (a radiogroup), so the label does not repeat it in the group's name. */\n  requiredOnControl?: boolean;\n  className?: string;\n  style?: CSSProperties;\n  classNames?: Pick<FieldClassNames, \"label\" | \"description\" | \"error\">;\n  children: (aria: ChoiceFieldAria) => ReactNode;\n}) {\n  const auto = useId();\n  const labelId = `choice-${auto}-label`;\n  const messageId = error || description ? `choice-${auto}-message` : undefined;\n  return (\n    <div data-slot={slot} data-invalid={error ? \"\" : undefined} style={style} className={cn(styles.field, className)}>\n      <Label\n        id={labelId}\n        required={required}\n        requiredOnControl={requiredOnControl}\n        className={cn(\"text-label\", hideLabel && \"sr-only\", classNames?.label)}\n      >\n        {label}\n      </Label>\n      {children({\n        \"aria-labelledby\": labelId,\n        ...(messageId ? { \"aria-describedby\": messageId } : {}),\n        ...(error ? { \"aria-invalid\": true as const } : {}),\n      })}\n      <FieldMessage\n        error={error}\n        description={description}\n        errorId={messageId}\n        descriptionId={messageId}\n        classNames={classNames}\n      />\n    </div>\n  );\n}\n"},{"path":"beamline/components/_shared/glide.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/glide.tsx","content":"\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type RefObject } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport overlay from \"./overlay.module.css\";\n\n/**\n * The gliding highlight for a list whose active row is tracked in state (command menu, listbox): one plate travels to\n * the active row for the arrow keys and the pointer alike, over --pui-duration-fast. It appears in place (no travel)\n * the first time and whenever `reset` changes (a new page or a new query), so typing never sends it flying.\n * The list must be the plate's offset parent (position: relative); rows stay above it (overlay.item is z-index 1).\n */\nexport function GlidePlate({\n  list,\n  selector,\n  reset,\n  axis = \"y\",\n  className,\n}: {\n  list: RefObject<HTMLElement | null>;\n  /** Finds the active row inside `list`; null when nothing is active. */\n  selector: string | null;\n  reset?: unknown;\n  /** \"y\" for a list (the plate spans the width); \"xy\" for a row or grid of cells (it takes each cell's size). */\n  axis?: \"y\" | \"xy\";\n  className?: string;\n}) {\n  const [plate, setPlate] = useState<{ x: number; y: number; w: number; h: number; jump: boolean } | null>(null);\n  const lastReset = useRef(reset);\n  const self = useRef<HTMLSpanElement>(null);\n  // eslint-disable-next-line react-hooks/exhaustive-deps -- every render on purpose; setPlate bails out when unchanged\n  useLayoutEffect(() => {\n    // the list holds the plate, and React attaches a parent's ref only after its children's layout effects: on the\n    // first commit the list is the plate's own parent (it placed itself only when something rendered again later)\n    const host = list.current ?? self.current?.parentElement;\n    const row = selector ? host?.querySelector<HTMLElement>(selector) : null;\n    const fresh = lastReset.current !== reset;\n    lastReset.current = reset;\n    if (!row) {\n      setPlate((p) => (p && p.h ? { ...p, h: 0 } : p));\n      return;\n    }\n    const x = row.offsetLeft,\n      y = row.offsetTop,\n      w = row.offsetWidth,\n      h = row.offsetHeight;\n    setPlate((p) =>\n      p && p.x === x && p.y === y && p.w === w && p.h === h && !fresh ? p : { x, y, w, h, jump: fresh || !p?.h },\n    );\n    // every render: rows can arrive or change height without the selector changing; an unchanged place keeps the state\n  });\n  return (\n    <span\n      ref={self}\n      aria-hidden\n      data-slot=\"list-highlight\"\n      className={cn(overlay.glide, className)}\n      data-on={plate?.h ? \"\" : undefined}\n      data-jump={plate?.jump ? \"\" : undefined}\n      data-axis={axis}\n      style={\n        !plate\n          ? undefined\n          : axis === \"y\"\n            ? { transform: `translateY(${plate.y}px)`, height: plate.h || undefined }\n            : { transform: `translate(${plate.x}px, ${plate.y}px)`, width: plate.w, height: plate.h || undefined }\n      }\n    />\n  );\n}\n\n/**\n * A GlidePlate for lists whose highlight is the pointer or keyboard focus rather than state (time picker, plain option\n * lists): it follows the row under the pointer or holding focus, and fades when neither is in the list. Rows drop\n * their own hover and focus tint.\n */\nexport function FollowPlate({\n  list,\n  rows,\n  className,\n}: {\n  list: RefObject<HTMLElement | null>;\n  /** Matches a row inside `list`, e.g. \"[role=option]\". */\n  rows: string;\n  className?: string;\n}) {\n  const [hot, setHot] = useState<string | null>(null);\n  // after the commit, when the list's ref is attached (a layout effect here ran before it and never listened)\n  useEffect(() => {\n    const el = list.current;\n    if (!el) return;\n    let n = 0;\n    const mark = (target: EventTarget | null) => {\n      const row = (target as HTMLElement | null)?.closest?.<HTMLElement>(rows);\n      if (!row || !el.contains(row)) return;\n      row.dataset.glideKey ??= String(++n);\n      setHot(row.dataset.glideKey);\n    };\n    const over = (e: Event) => mark(e.target);\n    const leave = () => {\n      if (!el.contains(document.activeElement)) setHot(null);\n    };\n    const blur = (e: FocusEvent) => {\n      if (!el.contains(e.relatedTarget as Node)) setHot(null);\n    };\n    el.addEventListener(\"pointerover\", over);\n    el.addEventListener(\"focusin\", over);\n    el.addEventListener(\"pointerleave\", leave);\n    el.addEventListener(\"focusout\", blur);\n    return () => {\n      el.removeEventListener(\"pointerover\", over);\n      el.removeEventListener(\"focusin\", over);\n      el.removeEventListener(\"pointerleave\", leave);\n      el.removeEventListener(\"focusout\", blur);\n    };\n  }, [list, rows]);\n  return <GlidePlate list={list} selector={hot ? `[data-glide-key=\"${hot}\"]` : null} className={className} />;\n}\n"},{"path":"beamline/components/_shared/icon-action.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/icon-action.module.css","content":"@layer properties, theme, base, components, utilities;\n/* The small square action that sits inside another element: a field's clear and open buttons, a chip's remove, a\n   toast's close, a carousel's previous and next. One recipe so every one of them answers alike: quiet ink at rest,\n   the hover plate at once, the press at once, both let go over --pui-motion-out. Its corner is the chip radius: under\n   the nested-corner rule (.nest-corner) a part whose own corner is the chip never changes inside a rounded frame. */\n@layer components {\n  .root {\n    position: relative;\n    display: grid;\n    flex-shrink: 0;\n    place-items: center;\n    inline-size: var(--_size);\n    block-size: var(--_size);\n    border-radius: var(--pui-radius-chip);\n    color: var(--pui-ink-3);\n    /* opacity and translate stay in the list so a .reveal action keeps its own arrival whichever rule wins */\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      transform var(--pui-motion-out) var(--pui-ease-default),\n      opacity var(--pui-motion-out) var(--pui-ease-default),\n      translate var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root:hover:not(:disabled),\n  .root[data-pressed] {\n    background-color: var(--pui-hover);\n    color: var(--pui-ink);\n  }\n  .root:hover {\n    transition-duration: 0ms;\n  }\n  .root:active:not(:disabled) {\n    transform: scale(var(--pui-press-scale));\n    transition-duration: 0ms;\n  }\n  .root:disabled {\n    cursor: default;\n    opacity: 0.4;\n  }\n  .root[data-tone=\"ink-2\"] {\n    color: var(--pui-ink-2);\n  }\n  .root[data-tone=\"inherit\"] {\n    color: inherit;\n  }\n  .root[data-tone=\"inherit\"]:hover:not(:disabled) {\n    color: inherit;\n  }\n  /* on an inverted (ink) surface: inverse ink, a faint inverse plate */\n  .root[data-tone=\"inverse\"] {\n    color: color-mix(in oklab, var(--pui-ink-inverse) 72%, transparent);\n  }\n  .root[data-tone=\"inverse\"]:hover:not(:disabled),\n  .root[data-tone=\"inverse\"][data-pressed] {\n    background-color: color-mix(in oklab, var(--pui-ink-inverse) 12%, transparent);\n    color: var(--pui-ink-inverse);\n  }\n\n  .xs {\n    --_size: 1.25rem;\n  }\n  /* a 20px action still takes a 24px hit (WCAG 2.5.8): the bleed is outside the drawn square */\n  .xs::after {\n    position: absolute;\n    inset: calc((var(--pui-hit-min) - 1.25rem) / -2);\n    content: \"\";\n  }\n  .sm {\n    --_size: 1.5rem;\n  }\n  .md {\n    --_size: 1.75rem;\n  }\n  .lg {\n    --_size: 2rem;\n  }\n}\n"},{"path":"beamline/components/_shared/icon-action.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/icon-action.tsx","content":"\"use client\";\n// organism-allow-file: raw-control -- the shared part every element draws this control from\n\nimport type { ButtonHTMLAttributes, Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./icon-action.module.css\";\n\n/** 20 · 24 · 28 · 32 px. A 20px action keeps a 24px hit outside its square. */\nexport type IconActionSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nexport interface IconActionProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  /** The action's name: the button holds only an icon. */\n  \"aria-label\": string;\n  size?: IconActionSize;\n  /** Rest ink: ink-3 (the default, an action beside content), ink-2 (an action that is the row's point), inverse (on\n   *  an inverted ink surface) or inherit (on a painted surface that brings its own colours). */\n  tone?: \"ink-3\" | \"ink-2\" | \"inverse\" | \"inherit\";\n  /** Holds the hover plate while what it opens is open, or while it is switched on. */\n  pressed?: boolean;\n  ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * The small square icon action inside another element (clear, remove, dismiss, previous, next, retry). One recipe for\n * all of them; a full-size action with words is a Button, and Button with iconOnly is the standalone square.\n */\nexport function IconAction({\n  size = \"sm\",\n  tone = \"ink-3\",\n  pressed,\n  type = \"button\",\n  className,\n  ref,\n  ...props\n}: IconActionProps) {\n  return (\n    <button\n      ref={ref}\n      type={type}\n      data-tone={tone === \"ink-3\" ? undefined : tone}\n      data-pressed={pressed ? \"\" : undefined}\n      className={cn(styles.root, styles[size], className)}\n      {...props}\n    />\n  );\n}\n"},{"path":"beamline/components/_shared/menu.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/menu.tsx","content":"\"use client\";\n\nimport {\n  Fragment,\n  useCallback,\n  useRef,\n  useState,\n  type ComponentType,\n  type FocusEvent,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport { Check, ChevronRight } from \"lucide-react\";\nimport { useDirection } from \"@radix-ui/react-direction\";\nimport { cn } from \"../../lib/utils\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport overlay from \"./overlay.module.css\";\n\nexport interface MenuClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the trigger part; no override by default. */\n  trigger?: string;\n  /** Classes for the content part; no override by default. */\n  content?: string;\n  /** Classes for the submenu part; no override by default. */\n  submenu?: string;\n  /** Classes for the indicator part; no override by default. */\n  indicator?: string;\n  /** Classes for the item part; no override by default. */\n  item?: string;\n  /** Classes for the label part; no override by default. */\n  label?: string;\n  /** Classes for the shortcut part; no override by default. */\n  shortcut?: string;\n  /** Classes for the check part; no override by default. */\n  check?: string;\n  /** Classes for the separator part; no override by default. */\n  separator?: string;\n  /** Classes for the heading part; no override by default. */\n  heading?: string;\n  /** Classes for the chevron part; no override by default. */\n  chevron?: string;\n}\n\n/** One row of any menu in the system (dropdown, context menu, menu bar). The same data renders the same everywhere. */\nexport interface MenuItem {\n  /** Stable key; the label is used when absent. */\n  id?: string;\n  /** Text displayed for this item. Required. */\n  label: ReactNode;\n  /** Icon element displayed with this item. Omitted by default. */\n  icon?: ReactNode;\n  /** A shortcut shown at the end, e.g. \"⌘D\". The app binds the key. */\n  shortcut?: string;\n  /** Runs when this item is activated. Omitted by default. */\n  onSelect?: (event: Event) => void;\n  /** Whether this option is unavailable for selection. Omitted by default. */\n  disabled?: boolean;\n  /** danger: a destructive action (red). */\n  tone?: \"danger\";\n  /** A line above this item. */\n  separatorBefore?: boolean;\n  /** A small heading above this item, starting a group. */\n  heading?: string;\n  /** A checkable item; shows a check when true. */\n  checked?: boolean;\n  /** checkbox (default when checked is set) stays open when toggled; radio is one of a set and closes the menu. */\n  type?: \"checkbox\" | \"radio\";\n  /** A submenu. */\n  items?: MenuItem[];\n}\n\ntype Part = ComponentType<any>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Radix menu namespaces share part names, not prop types\n/** The Radix parts a menu needs; DropdownMenu, ContextMenu and Menubar all provide them. */\nexport interface MenuKit {\n  Content: Part;\n  SubContent: Part;\n  Item: Part;\n  CheckboxItem: Part;\n  ItemIndicator: Part;\n  Sub: Part;\n  SubTrigger: Part;\n  Separator: Part;\n  Label: Part;\n  Portal: Part;\n}\n\n/** The reading direction a menu should open in: an explicit one, else the direction in force around its trigger (read\n *  when the trigger mounts and again as it is pressed, right-clicked or keyed), else a Direction provider. Without it\n *  Radix falls back to \"ltr\", so a portalled menu on an RTL page reads left to right, opens submenus to the right and\n *  answers → instead of ←. Spread `triggerProps` on the Radix trigger and pass `dir` to the Radix root. */\nexport function useMenuDirection(explicit?: \"ltr\" | \"rtl\") {\n  const provided = useDirection();\n  const [around, setAround] = useState<\"ltr\" | \"rtl\">();\n  const read = useCallback((el: Element | null) => {\n    if (el) setAround(getComputedStyle(el).direction === \"rtl\" ? \"rtl\" : \"ltr\");\n  }, []);\n  const measure = useCallback((e: { currentTarget: Element }) => read(e.currentTarget), [read]);\n  return {\n    dir: explicit ?? around ?? provided,\n    triggerProps: {\n      ref: read,\n      onPointerDownCapture: measure,\n      onKeyDownCapture: measure,\n      onContextMenuCapture: measure,\n    },\n  };\n}\n\n/** The floating list: one surface and one highlight plate that glides between rows, under the pointer and the arrow\n *  keys alike. Extra props (align, side, sideOffset) go to the Radix content part. */\nexport function MenuSurface({\n  kit,\n  sub = false,\n  label,\n  className,\n  classNames,\n  children,\n  ref,\n  ...rest\n}: {\n  kit: MenuKit;\n  sub?: boolean;\n  label?: string;\n  className?: string;\n  classNames?: MenuClassNames;\n  children: ReactNode;\n  ref?: Ref<HTMLDivElement>;\n} & Record<string, unknown>) {\n  const surface = useRef<HTMLDivElement | null>(null);\n  // the portal's presence passes its own ref (it watches the exit animation); keep both\n  const attach = useComposedRef(surface, ref);\n  const [plate, setPlate] = useState<{ y: number; h: number; jump: boolean } | null>(null);\n  const Part = sub ? kit.SubContent : kit.Content;\n  const { onFocusOutside, ...others } = rest as { onFocusOutside?: (event: Event) => void };\n  return (\n    <Part\n      ref={attach}\n      data-slot={sub ? \"menu-submenu\" : \"menu\"}\n      aria-label={label}\n      collisionPadding={8}\n      loop\n      {...(sub ? { sideOffset: 2, alignOffset: -4 } : {})}\n      {...others}\n      // a surface still playing its exit is already closed: the focus that moves into the next menu of a menu bar\n      // must not count as \"outside\" and close the whole bar (Radix would reset the bar's open menu to none)\n      onFocusOutside={(event: Event) => {\n        if (surface.current?.dataset.state === \"closed\") event.preventDefault();\n        onFocusOutside?.(event);\n      }}\n      className={cn(\n        overlay.surface,\n        overlay.menu,\n        \"relative min-w-[11rem]\",\n        sub ? classNames?.submenu : classNames?.content,\n        className,\n      )}\n      // React events cross portals: only rows of this surface move its plate (a submenu has its own)\n      onFocusCapture={(e: FocusEvent<HTMLDivElement>) => {\n        const item = (e.target as HTMLElement).closest<HTMLElement>(\"[data-menu-item]\");\n        if (item && !surface.current?.contains(item)) return;\n        if (!item) {\n          setPlate((p) => (p ? { ...p, h: 0 } : p));\n          return;\n        }\n        setPlate((p) => ({ y: item.offsetTop, h: item.offsetHeight, jump: !p?.h }));\n      }}\n      // while a submenu is open, its trigger keeps the plate\n      onPointerLeave={() => {\n        if (!surface.current?.querySelector(\":scope > [data-menu-item][data-state=open]\"))\n          setPlate((p) => (p ? { ...p, h: 0 } : p));\n      }}\n    >\n      <span\n        aria-hidden\n        data-slot=\"menu-indicator\"\n        className={cn(overlay.glide, classNames?.indicator)}\n        data-on={plate?.h ? \"\" : undefined}\n        data-jump={plate?.jump ? \"\" : undefined}\n        style={plate ? { transform: `translateY(${plate.y}px)`, height: plate.h || undefined } : undefined}\n      />\n      {children}\n    </Part>\n  );\n}\n\n/** Rows for a MenuItem list, in any kit. */\nexport function MenuItems({\n  kit,\n  items,\n  container,\n  classNames,\n}: {\n  kit: MenuKit;\n  items: MenuItem[];\n  container?: HTMLElement | null;\n  classNames?: MenuClassNames;\n}) {\n  return items.map((item, i) => {\n    const tone = item.tone;\n    const common = {\n      \"data-menu-item\": \"\",\n      \"data-slot\": \"menu-item\",\n      \"data-glide\": \"\",\n      \"data-tone\": tone,\n      className: cn(overlay.item, classNames?.item),\n      disabled: item.disabled,\n    };\n    const content = (\n      <>\n        {item.icon}\n        <span data-slot=\"menu-label\" dir=\"auto\" className={cn(\"min-w-0 truncate\", classNames?.label)}>\n          {item.label}\n        </span>\n        {item.shortcut && (\n          <span data-slot=\"menu-shortcut\" dir=\"ltr\" className={cn(overlay.shortcut, classNames?.shortcut)}>\n            {item.shortcut}\n          </span>\n        )}\n      </>\n    );\n    let node: ReactNode;\n    if (item.items?.length) {\n      node = (\n        <kit.Sub>\n          <kit.SubTrigger {...common}>\n            {content}\n            <ChevronRight data-slot=\"menu-chevron\" className={cn(\"ms-auto rtl:rotate-180\", classNames?.chevron)} />\n          </kit.SubTrigger>\n          {container !== null && (\n            <kit.Portal container={container}>\n              <MenuSurface kit={kit} sub classNames={classNames}>\n                <MenuItems kit={kit} items={item.items} container={container} classNames={classNames} />\n              </MenuSurface>\n            </kit.Portal>\n          )}\n        </kit.Sub>\n      );\n    } else if (item.checked !== undefined && item.type === \"radio\") {\n      node = (\n        <kit.Item {...common} role=\"menuitemradio\" aria-checked={item.checked} onSelect={item.onSelect}>\n          {content}\n          <span data-slot=\"menu-check\" className={cn(overlay.check, classNames?.check)}>\n            {item.checked && <Check />}\n          </span>\n        </kit.Item>\n      );\n    } else if (item.checked !== undefined) {\n      node = (\n        <kit.CheckboxItem\n          {...common}\n          checked={item.checked}\n          onSelect={(e: Event) => {\n            e.preventDefault();\n            item.onSelect?.(e);\n          }}\n        >\n          {content}\n          <kit.ItemIndicator data-slot=\"menu-check\" className={cn(overlay.check, classNames?.check)}>\n            <Check />\n          </kit.ItemIndicator>\n        </kit.CheckboxItem>\n      );\n    } else {\n      node = (\n        <kit.Item {...common} onSelect={item.onSelect}>\n          {content}\n        </kit.Item>\n      );\n    }\n    return (\n      <Fragment key={item.id ?? (typeof item.label === \"string\" ? item.label : i)}>\n        {item.separatorBefore && i > 0 && (\n          <kit.Separator data-slot=\"menu-separator\" className={cn(overlay.separator, classNames?.separator)} />\n        )}\n        {item.heading && (\n          <kit.Label data-slot=\"menu-heading\" className={cn(overlay.heading, classNames?.heading)}>\n            {item.heading}\n          </kit.Label>\n        )}\n        {node}\n      </Fragment>\n    );\n  });\n}\n"},{"path":"beamline/components/_shared/messages.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy for this helper only. */\nexport const english = {\n  results: {\n    one: \"{count} result\",\n    other: \"{count} results\",\n  },\n  noMatch: \"No match for {query}\",\n  fieldOver: \"{count} over\",\n  fieldFull: \"Limit reached\",\n  fieldLeft: \"{count} left\",\n  foldMore: \"{list} and {count} more\",\n  foldShow: \"Show {count} more {noun}: {list}\",\n  statusLabel: \"Checking\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/_shared/overlay.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/overlay.module.css","content":"@layer properties, theme, base, components, utilities;\n/* the source morph's moving parts: the clip's four sides, the radius and the offset back to the trigger */\n@property --ci-t {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-r {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-b {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-l {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-x {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n@property --ci-y {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n@layer components {\n  /* Every floating layer in the system: one surface (painted by the overlay role of the look in force), one menu-item\n   look, one scrim, one enter and exit. A layer placed against a trigger opens out of it (the source morph below); one\n   without a placement arrives in --pui-motion-in scaling from its origin. Each leaves in --pui-motion-out; Radix keeps\n   it mounted until the exit animation ends. Reduced motion makes both instant (the duration tokens become 1ms). */\n  .surface {\n    z-index: var(--pui-z-popover);\n    min-width: 8rem;\n    /* never past the screen: as wide and as tall as the room beside the trigger (Radix measures it) */\n    max-width: var(--radix-popper-available-width, none);\n    max-height: var(--radix-popper-available-height, 70vh);\n    overflow: auto;\n    border-radius: var(--pui-overlay-radius, var(--pui-radius-card));\n    color: var(--pui-ink);\n    transform-origin: var(--radix-popper-transform-origin, var(--radix-dropdown-menu-content-transform-origin, center));\n    outline: none;\n  }\n  .surface[data-state=\"open\"],\n  .surface[data-state=\"delayed-open\"],\n  .surface[data-state=\"instant-open\"] {\n    animation: pop-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  .surface[data-state=\"closed\"] {\n    animation: pop-out var(--pui-motion-out) var(--pui-ease-default) forwards;\n  }\n\n  /* The source morph (research/morphing-interfaces-2026-10-04, M3): a layer placed against a trigger does not fade in\n     beside it, it opens out of it. It starts as the trigger's own box (Radix gives its size and the side and alignment\n     it was placed on), offset back onto the trigger, and grows: the side nearest the trigger is there at once, the\n     sides across follow, the far side arrives last (~200 ms), so the rows by the trigger are readable within the\n     ruled 90 ms while the outline settles. Content fades in over those 90 ms. It closes back into the trigger in the\n     ruled 180 ms. The clip ends 48px outside the box so the layer's shadow is never cut. Reduced motion: instant. */\n  .surface[data-side] {\n    --_aw: var(--radix-popper-anchor-width, 0px);\n    --_ah: var(--radix-popper-anchor-height, 0px);\n    --_gap: 6px;\n    --from-t: -48px;\n    --from-r: -48px;\n    --from-b: -48px;\n    --from-l: -48px;\n    --from-x: 0px;\n    --from-y: 0px;\n    --d-near: calc(var(--pui-duration-standard) * 0.55);\n    --d-cross: calc(var(--pui-duration-standard) * 0.85);\n    --d-far: var(--pui-duration-standard);\n    clip-path: inset(\n      var(--ci-t) var(--ci-r) var(--ci-b) var(--ci-l) round var(--pui-overlay-radius, var(--pui-radius-card))\n    );\n    translate: var(--ci-x) var(--ci-y);\n  }\n  .surface[data-side=\"bottom\"] {\n    --from-t: 0px;\n    --from-b: calc(100% - var(--_ah));\n    --from-y: calc(-1 * (var(--_ah) + var(--_gap)));\n    --d-t: var(--d-near);\n    --d-b: var(--d-far);\n    --d-l: var(--d-cross);\n    --d-r: var(--d-cross);\n  }\n  .surface[data-side=\"top\"] {\n    --from-b: 0px;\n    --from-t: calc(100% - var(--_ah));\n    --from-y: calc(var(--_ah) + var(--_gap));\n    --d-b: var(--d-near);\n    --d-t: var(--d-far);\n    --d-l: var(--d-cross);\n    --d-r: var(--d-cross);\n  }\n  .surface[data-side=\"right\"] {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n    --from-x: calc(-1 * (var(--_aw) + var(--_gap)));\n    --d-l: var(--d-near);\n    --d-r: var(--d-far);\n    --d-t: var(--d-cross);\n    --d-b: var(--d-cross);\n  }\n  .surface[data-side=\"left\"] {\n    --from-r: 0px;\n    --from-l: calc(100% - var(--_aw));\n    --from-x: calc(var(--_aw) + var(--_gap));\n    --d-r: var(--d-near);\n    --d-l: var(--d-far);\n    --d-t: var(--d-cross);\n    --d-b: var(--d-cross);\n  }\n  /* alignment along the trigger: which part of the layer the trigger sits over */\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"start\"] {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"end\"] {\n    --from-l: calc(100% - var(--_aw));\n    --from-r: 0px;\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"start\"]:dir(rtl) {\n    --from-l: calc(100% - var(--_aw));\n    --from-r: 0px;\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"end\"]:dir(rtl) {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"center\"] {\n    --from-l: calc((100% - var(--_aw)) / 2);\n    --from-r: calc((100% - var(--_aw)) / 2);\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"start\"] {\n    --from-t: 0px;\n    --from-b: calc(100% - var(--_ah));\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"end\"] {\n    --from-t: calc(100% - var(--_ah));\n    --from-b: 0px;\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"center\"] {\n    --from-t: calc((100% - var(--_ah)) / 2);\n    --from-b: calc((100% - var(--_ah)) / 2);\n  }\n  .surface[data-side]:is([data-state=\"open\"], [data-state=\"delayed-open\"], [data-state=\"instant-open\"]) {\n    animation:\n      grow-t var(--d-t) var(--pui-ease-out-strong),\n      grow-r var(--d-r) var(--pui-ease-out-strong),\n      grow-b var(--d-b) var(--pui-ease-out-strong),\n      grow-l var(--d-l) var(--pui-ease-out-strong),\n      grow-xy var(--d-cross) var(--pui-ease-out-strong),\n      settling var(--d-far) step-end;\n  }\n  .surface[data-side]:is([data-state=\"open\"], [data-state=\"delayed-open\"], [data-state=\"instant-open\"]) > * {\n    animation: content-in var(--pui-motion-in) var(--pui-ease-default) both;\n  }\n  /* one duration for every part of the exit: the layer unmounts when the first of them ends */\n  .surface[data-side][data-state=\"closed\"] {\n    animation: shrink var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  .surface[data-side][data-state=\"closed\"] > * {\n    animation: content-out var(--pui-motion-in) var(--pui-ease-default) forwards;\n  }\n  @keyframes grow-t {\n    from {\n      --ci-t: var(--from-t);\n    }\n  }\n  @keyframes grow-r {\n    from {\n      --ci-r: var(--from-r);\n    }\n  }\n  @keyframes grow-b {\n    from {\n      --ci-b: var(--from-b);\n    }\n  }\n  @keyframes grow-l {\n    from {\n      --ci-l: var(--from-l);\n    }\n  }\n  @keyframes grow-xy {\n    from {\n      --ci-x: var(--from-x);\n      --ci-y: var(--from-y);\n    }\n  }\n  /* it starts over its own trigger: until it has grown, the press that opened it must not land on a row */\n  @keyframes settling {\n    0%,\n    100% {\n      pointer-events: none;\n    }\n  }\n  @keyframes shrink {\n    to {\n      --ci-t: var(--from-t);\n      --ci-r: var(--from-r);\n      --ci-b: var(--from-b);\n      --ci-l: var(--from-l);\n      --ci-x: var(--from-x);\n      --ci-y: var(--from-y);\n      opacity: 0;\n    }\n  }\n  @keyframes content-in {\n    from {\n      opacity: 0;\n    }\n  }\n  @keyframes content-out {\n    to {\n      opacity: 0;\n    }\n  }\n  .padded {\n    padding: var(--pui-pad-popover);\n  }\n  .menu {\n    padding: var(--pui-space-1);\n  }\n  @keyframes pop-in {\n    from {\n      opacity: 0;\n      transform: scale(0.97);\n    }\n  }\n  @keyframes pop-out {\n    to {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n\n  /* rows (and the gliding plate) nest in the list: its radius minus its padding, never rounder than a chip; on the\n     row itself, so any list that uses .item gets it */\n  .item,\n  .glide {\n    --_item-radius: min(\n      var(--pui-radius-chip),\n      calc(var(--pui-overlay-radius, var(--pui-radius-card)) - var(--pui-space-1))\n    );\n  }\n\n  /* menu rows: shared by dropdown, context menu, menubar, select, combobox lists */\n  .item {\n    position: relative;\n    z-index: 1;\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-2);\n    min-height: var(--pui-control-height-sm);\n    padding: 0 var(--pui-space-2);\n    /* nested in the list: the overlay's radius minus its padding, never rounder than a chip */\n    border-radius: var(--_item-radius);\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    cursor: default;\n    user-select: none;\n    outline: none;\n  }\n  /* on a touch screen a row is a finger's target: the touch height (44px) from the tokens; a mouse keeps the dense row */\n  @media (pointer: coarse) {\n    .item {\n      min-height: var(--pui-control-height-touch);\n    }\n  }\n  /* a destructive row speaks in the danger ink only while it can be used: disabled wins (muted ink, no alarm for an\n     action that cannot happen) */\n  .item[data-tone=\"danger\"]:not([data-disabled]) {\n    color: var(--pui-red);\n  }\n  .item[data-disabled] {\n    color: var(--pui-ink-3);\n    pointer-events: none;\n  }\n  .item :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n  }\n  .item[data-tone=\"danger\"]:not([data-disabled]) :where(svg) {\n    color: currentColor;\n  }\n  .item[data-highlighted]:not([data-glide]) {\n    background: var(--pui-hover-2);\n  }\n  /* a press is acknowledged at once, over the plate, before the menu closes or the row toggles */\n  .item:active:not([data-disabled]) {\n    background: var(--pui-hover);\n  }\n  .shortcut {\n    margin-inline-start: auto;\n    padding-inline-start: var(--pui-space-4);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    letter-spacing: var(--pui-tracking-mono-caps);\n  }\n  .heading {\n    padding: var(--pui-space-1-5) var(--pui-space-2) var(--pui-space-1);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-medium);\n  }\n  .separator {\n    height: 1px;\n    margin: var(--pui-space-1) calc(var(--pui-space-1) * -1);\n    background: var(--pui-line);\n  }\n  .check {\n    margin-inline-start: auto;\n    color: var(--pui-accent) !important;\n  }\n  /* a check arrives in its row as the value changes (at once): it grows from the middle over the arrival */\n  .check :where(svg) {\n    animation: check-in var(--pui-motion-in) var(--pui-ease-out-strong);\n  }\n  @keyframes check-in {\n    from {\n      opacity: 0;\n      scale: calc(1 - 0.5 * var(--pui-motion-travel, 1));\n    }\n  }\n  /* the gliding highlight: one plate moves between rows for the pointer and the arrow keys; it appears in place */\n  .glide {\n    position: absolute;\n    z-index: 0;\n    inset-inline: var(--pui-space-1);\n    top: 0;\n    border-radius: var(--_item-radius);\n    background: var(--pui-hover-2);\n    pointer-events: none;\n    opacity: 0;\n    transition: opacity var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .glide[data-on] {\n    opacity: 1;\n    transition:\n      transform var(--pui-duration-fast) var(--pui-ease-standard),\n      height var(--pui-duration-fast) var(--pui-ease-standard),\n      opacity 0ms;\n  }\n  .glide[data-jump] {\n    transition: opacity 0ms;\n  }\n  /* a plate over a row or grid of cells takes each cell's size and travels both ways */\n  .glide[data-axis=\"xy\"] {\n    inset-inline: auto;\n    left: 0;\n  }\n  .glide[data-axis=\"xy\"][data-on] {\n    transition:\n      transform var(--pui-duration-fast) var(--pui-ease-standard),\n      width var(--pui-duration-fast) var(--pui-ease-standard),\n      height var(--pui-duration-fast) var(--pui-ease-standard),\n      opacity 0ms;\n  }\n\n  /* a layer that has begun to close is out of the way at once: for the length of its exit it takes no pointer, so a tap\n     meant for the page behind (the next row, the button the panel covered) lands there, never on the leaving ghost.\n     Focus has already gone back (Radix) and it unmounts when the exit ends. */\n  .scrim[data-state=\"closed\"],\n  .surface[data-state=\"closed\"],\n  .dialog[data-state=\"closed\"],\n  .sheet[data-state=\"closed\"] {\n    pointer-events: none;\n  }\n\n  /* modal layers */\n  .scrim {\n    position: fixed;\n    inset: 0;\n    z-index: var(--pui-z-overlay);\n    background: var(--pui-scrim);\n    backdrop-filter: blur(var(--pui-blur-overlay));\n  }\n  .scrim[data-state=\"open\"] {\n    animation: fade-in var(--pui-fade-in) var(--pui-ease-default);\n  }\n  .scrim[data-state=\"closed\"] {\n    animation: fade-out var(--pui-fade-out) var(--pui-ease-default) forwards;\n  }\n  @keyframes fade-in {\n    from {\n      opacity: 0;\n    }\n  }\n  @keyframes fade-out {\n    to {\n      opacity: 0;\n    }\n  }\n  .dialog {\n    /* the content answers to the dialog's own width (a footer's buttons share the row on a phone) */\n    container-type: inline-size;\n    position: fixed;\n    z-index: var(--pui-z-modal);\n    left: 50%;\n    top: 50%;\n    width: min(calc(100vw - var(--pui-space-8)), var(--pui-dialog-width, var(--_dialog-width, 440px)));\n    max-height: calc(100dvh - var(--pui-space-12));\n    overflow: hidden;\n    translate: -50% -50%;\n    border-radius: var(--pui-dialog-radius, var(--pui-radius-window));\n    color: var(--pui-ink);\n    outline: none;\n    /* a size chosen while open (a step that needs more room) eases the width in place, centred */\n    transition: width var(--pui-duration-standard) var(--pui-ease-standard);\n  }\n  .dialog[data-state=\"open\"] {\n    animation: dialog-in var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .dialog[data-state=\"closed\"] {\n    animation: dialog-out var(--pui-motion-out) var(--pui-ease-default) forwards;\n  }\n  /* A dialog opened from something (use-open-from.ts) grows out of it: it starts as the opener's box, offset back onto\n     it, and grows to its own place over the considered duration while its content fades in; it closes back into the\n     opener in the ruled 180 ms. Shares the source morph's moving parts and guard with .surface. */\n  .dialog[data-open-from] {\n    clip-path: inset(\n      var(--ci-t) var(--ci-r) var(--ci-b) var(--ci-l) round var(--pui-dialog-radius, var(--pui-radius-window))\n    );\n    transform: translate(var(--ci-x), var(--ci-y));\n  }\n  .dialog[data-open-from][data-state=\"open\"] {\n    animation:\n      grow-t var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-r var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-b var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-l var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-xy calc(var(--pui-duration-considered) * 0.9) var(--pui-ease-out-strong),\n      settling var(--pui-duration-considered) step-end;\n  }\n  .dialog[data-open-from][data-state=\"open\"] > * {\n    animation: content-in var(--pui-duration-standard) var(--pui-ease-default) calc(var(--pui-duration-standard) * 0.35)\n      both;\n  }\n  .dialog[data-open-from][data-state=\"closed\"] {\n    animation: shrink var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  .dialog[data-open-from][data-state=\"closed\"] > * {\n    animation: content-out var(--pui-motion-in) var(--pui-ease-default) forwards;\n  }\n  /* A sheet: a panel from an edge. It slides in from its edge over the considered duration on the drawer curve and\n     goes back out to that edge in the ruled departure (a layer closes back into where it came from). The edge is\n     logical: an end (\"right\") sheet comes from the left in a right-to-left page. Its content answers to its width. */\n  .sheet {\n    --_sx: 0px;\n    --_sy: 0px;\n    container-type: inline-size;\n    /* where a finger has swiped it toward its edge (sheet.tsx); it settles back over the departure */\n    translate: var(--_swipe, 0px) 0;\n  }\n  /* a side panel leaves sideways pans to the swipe and keeps vertical scrolling native, in its body too */\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]),\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]) [data-slot=\"sheet-body\"] {\n    touch-action: pan-y;\n  }\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]) {\n    transition:\n      var(--m-transition),\n      translate var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .sheet[data-swiping] {\n    transition: none;\n  }\n  .sheet[data-side=\"right\"] {\n    --_sx: 100%;\n  }\n  .sheet[data-side=\"left\"] {\n    --_sx: -100%;\n  }\n  .sheet[data-side=\"right\"]:dir(rtl) {\n    --_sx: -100%;\n  }\n  .sheet[data-side=\"left\"]:dir(rtl) {\n    --_sx: 100%;\n  }\n  .sheet[data-side=\"top\"] {\n    --_sy: -100%;\n  }\n  .sheet[data-side=\"bottom\"] {\n    --_sy: 100%;\n  }\n  .sheet[data-state=\"open\"] {\n    animation: sheet-in var(--pui-duration-considered) var(--pui-ease-drawer) both;\n  }\n  .sheet[data-state=\"closed\"] {\n    animation: sheet-out var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  @keyframes sheet-in {\n    from {\n      translate: var(--_sx) var(--_sy);\n    }\n  }\n  @keyframes sheet-out {\n    to {\n      translate: var(--_sx) var(--_sy);\n    }\n  }\n  @keyframes dialog-in {\n    from {\n      opacity: 0;\n      transform: scale(0.96);\n    }\n  }\n  @keyframes dialog-out {\n    to {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n}\n\n/* Reduced motion: surfaces and dialogs fade without scaling or growing, over the fade tokens (restrained under reduced,\n   1ms under none; APPEARANCE A06). */\n@media (prefers-reduced-motion: reduce) {\n  .surface,\n  .dialog,\n  .sheet {\n    animation-name: fade-in !important;\n    animation-duration: var(--pui-fade-in) !important;\n  }\n  .surface[data-state=\"closed\"],\n  .dialog[data-state=\"closed\"],\n  .sheet[data-state=\"closed\"] {\n    animation-name: fade-out !important;\n    animation-duration: var(--pui-fade-out) !important;\n  }\n}\n@layer components {\n  :global(.pui[data-motion=\"reduced\"]) .surface,\n  :global(.pui[data-motion=\"none\"]) .surface,\n  :global(.pui[data-motion=\"reduced\"]) .dialog,\n  :global(.pui[data-motion=\"none\"]) .dialog,\n  :global(.pui[data-motion=\"reduced\"]) .sheet,\n  :global(.pui[data-motion=\"none\"]) .sheet {\n    animation-name: fade-in;\n    animation-duration: var(--pui-fade-in);\n  }\n  :global(.pui[data-motion=\"reduced\"]) .surface[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .surface[data-state=\"closed\"],\n  :global(.pui[data-motion=\"reduced\"]) .dialog[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .dialog[data-state=\"closed\"],\n  :global(.pui[data-motion=\"reduced\"]) .sheet[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .sheet[data-state=\"closed\"] {\n    animation-name: fade-out;\n    animation-duration: var(--pui-fade-out);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.surface {\n  composes: m-overlay from global;\n}\n.dialog {\n  composes: m-overlay from global;\n}\n"},{"path":"beamline/components/_shared/roll.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/roll.ts","content":"/**\n * Rolls the text of a field or any text part from one value to the next (number field steps, a time segment): the old text leaves up and the new one\n * arrives from below for an increase, the other way for a decrease. The field's value changes at once; a still copy of\n * each text is drawn over the field for the roll and removed after, so typing and the caret are never touched.\n */\nconst running = new WeakMap<HTMLElement, () => void>();\n\nexport function rollText(\n  input: HTMLElement | null,\n  from: string,\n  to: string,\n  direction: 1 | -1,\n  { duration, easing }: { duration: number; easing: string },\n) {\n  if (!input || from === to || duration < 2 || !input.offsetParent) return;\n  running.get(input)?.();\n  const host = input.offsetParent as HTMLElement;\n  const cs = getComputedStyle(input);\n  const box = document.createElement(\"span\");\n  box.setAttribute(\"aria-hidden\", \"true\");\n  Object.assign(box.style, {\n    position: \"absolute\",\n    left: `${input.offsetLeft}px`,\n    top: `${input.offsetTop}px`,\n    width: `${input.offsetWidth}px`,\n    height: `${input.offsetHeight}px`,\n    overflow: \"hidden\",\n    pointerEvents: \"none\",\n  });\n  const line = (text: string) => {\n    const s = document.createElement(\"span\");\n    s.textContent = text;\n    Object.assign(s.style, {\n      position: \"absolute\",\n      inset: \"0\",\n      display: \"flex\",\n      alignItems: \"center\",\n      justifyContent:\n        cs.textAlign === \"right\" || cs.textAlign === \"end\"\n          ? \"flex-end\"\n          : cs.textAlign === \"center\"\n            ? \"center\"\n            : \"flex-start\",\n      paddingInline: `${cs.paddingLeft} ${cs.paddingRight}`,\n      font: cs.font,\n      fontVariantNumeric: cs.fontVariantNumeric,\n      letterSpacing: cs.letterSpacing,\n      color: cs.color,\n      whiteSpace: \"pre\",\n    });\n    return s;\n  };\n  const old = line(from),\n    next = line(to);\n  box.append(old, next);\n  host.appendChild(box);\n  const color = input.style.color;\n  input.style.color = \"transparent\";\n  const shift = 0.7 * direction;\n  const a = old.animate(\n    [\n      { transform: \"none\", opacity: 1 },\n      { transform: `translateY(${-shift * 100}%)`, opacity: 0 },\n    ],\n    { duration, easing },\n  );\n  next.animate(\n    [\n      { transform: `translateY(${shift * 100}%)`, opacity: 0 },\n      { transform: \"none\", opacity: 1 },\n    ],\n    { duration, easing },\n  );\n  const done = () => {\n    running.delete(input);\n    box.remove();\n    input.style.color = color;\n  };\n  running.set(input, done);\n  a.finished.then(done, done);\n}\n"},{"path":"beamline/components/_shared/screen.messages.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/screen.messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy for this helper only. */\nexport const english = {\n  tryAgain: \"Try again\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/_shared/screen.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/screen.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./screen.messages\";\nimport { useMemo } from \"react\";\nimport { useLocale, createFormatters } from \"../../runtime/localization\";\nimport { FALLBACK_LOCALE } from \"../../runtime/messages\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { RotateCw, TriangleAlert } from \"lucide-react\";\nimport { Button } from \"../button/button\";\nimport { EmptyState } from \"../empty-state/empty-state\";\nimport { Skeleton } from \"../skeleton/skeleton\";\nimport { cn } from \"../../lib/utils\";\n\nexport { useControllable } from \"../../hooks/use-controllable\";\n\n/*\n * The parts every account and admin screen shares (blocks team-members, roles-permissions, billing, api-keys and the\n * rest of that family): one piece of state with both forms, the screen's header with its one primary action, the\n * load-failed and loading panels, plain-language counts and times, and a removal that waits out its Undo before it\n * reaches your API. Internal: products import the blocks, never this file.\n */\n\n/** The message of a rejected promise, or the fallback when it has none. */\nexport const errorText = (e: unknown, fallback: string) => (e instanceof Error && e.message ? e.message : fallback);\nconst UNITS: [Intl.RelativeTimeFormatUnit, number][] = [\n  [\"year\", 31536000],\n  [\"month\", 2592000],\n  [\"week\", 604800],\n  [\"day\", 86400],\n  [\"hour\", 3600],\n  [\"minute\", 60],\n];\nexport function createScreenFormatters(locale: string) {\n  const intl = createFormatters(locale);\n  const plurals = new Intl.PluralRules(locale);\n  const day = (value: number) => intl.date(value, { month: \"short\", day: \"numeric\", year: \"numeric\" });\n  return {\n    plural: (count: number, one: string, many = `${one}s`) =>\n      `${intl.number(count)} ${plurals.select(count) === \"one\" ? one : many}`,\n    day,\n    dayTime: (value: number) =>\n      intl.date(value, { month: \"short\", day: \"numeric\", hour: \"2-digit\", minute: \"2-digit\" }),\n    ago: (value: number, now: number | null) => {\n      if (now === null) return day(value);\n      const seconds = (value - now) / 1000;\n      if (Math.abs(seconds) < 45) return intl.relative(0, \"second\", { numeric: \"auto\" });\n      for (const [unit, size] of UNITS)\n        if (Math.abs(seconds) >= size) return intl.relative(Math.round(seconds / size), unit, { numeric: \"auto\" });\n      return intl.relative(Math.round(seconds / 60), \"minute\", { numeric: \"auto\" });\n    },\n  };\n}\nexport const { plural, day, dayTime, ago } = createScreenFormatters(FALLBACK_LOCALE);\nexport function useScreenFormatters(override?: string) {\n  const locale = useLocale(override);\n  return useMemo(() => createScreenFormatters(locale), [locale]);\n}\n\nexport interface ScreenHeaderProps {\n  title: ReactNode;\n  description?: ReactNode;\n  /** Short facts beside the title (a count, a plan, a status badge). */\n  meta?: ReactNode;\n  /** The view's actions; the last one is its single primary action. */\n  actions?: ReactNode;\n  /** 1 when the screen is the page, 2 when it sits under your own page heading. */\n  level?: 1 | 2;\n  id?: string;\n  className?: string;\n}\n\n/** The screen's header: title, one line, facts, and the actions on the end (they drop under the title in a narrow room). */\nexport function ScreenHeader({ title, description, meta, actions, level = 1, id, className }: ScreenHeaderProps) {\n  const H = level === 1 ? \"h1\" : \"h2\";\n  return (\n    <header\n      data-slot=\"screen-header\"\n      className={cn(\"flex flex-wrap items-end gap-x-4 gap-y-3 border-b border-line-soft pb-4\", className)}\n    >\n      <div className=\"me-auto flex min-w-0 flex-col gap-1\">\n        <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n          <H id={id} tabIndex={-1} className=\"text-heading text-balance text-ink outline-none\">\n            {title}\n          </H>\n          {meta}\n        </div>\n        {description && <p className=\"max-w-[var(--pui-measure)] text-sm text-pretty text-ink-2\">{description}</p>}\n      </div>\n      {actions && (\n        <div data-slot=\"screen-actions\" className=\"flex flex-wrap items-center gap-2\">\n          {actions}\n        </div>\n      )}\n    </header>\n  );\n}\n\n/** Loading rows in the shape of a list, read once as \"Loading …\" by assistive tech. */\nexport function ScreenLoading({ label, rows = 5, className }: { label: string; rows?: number; className?: string }) {\n  return (\n    <div\n      data-slot=\"screen-loading\"\n      role=\"status\"\n      aria-label={label}\n      className={cn(\"m-surface flex flex-col gap-4 rounded-card p-5\", className)}\n    >\n      {Array.from({ length: rows }, (_, i) => (\n        <Skeleton key={i} loading avatar lines={2} label={i === 0 ? label : undefined} />\n      ))}\n    </div>\n  );\n}\n\n/** What could not load, why, and Try again (busy while it runs). */\nexport function ScreenProblem({\n  title,\n  error,\n  onRetry,\n  className,\n}: {\n  title: string;\n  error: ReactNode;\n  onRetry?: () => void | Promise<unknown>;\n  className?: string;\n}) {\n  const i18n = useMessages(\"_shared-screen\", englishMessages);\n\n  const [busy, setBusy] = useState(false);\n  const retry = async () => {\n    if (!onRetry) return;\n    setBusy(true);\n    try {\n      await onRetry();\n    } finally {\n      setBusy(false);\n    }\n  };\n  return (\n    <div\n      data-slot=\"screen-problem\"\n      role=\"alert\"\n      className={cn(\"m-surface grid min-h-60 place-items-center rounded-card p-6\", className)}\n    >\n      <EmptyState\n        icon={<TriangleAlert />}\n        title={title}\n        description={error}\n        action={\n          onRetry ? (\n            <Button size=\"sm\" variant=\"secondary\" loading={busy} onClick={retry}>\n              <RotateCw className=\"size-4\" />\n              {i18n(\"tryAgain\")}\n            </Button>\n          ) : undefined\n        }\n      />\n    </div>\n  );\n}\n\n/**\n * A removal that keeps its Undo: the item leaves the screen at once, and `commit` (your API) runs only when the Undo\n * window ends, so an undone removal never reaches the server. Leaving the screen commits what is pending.\n */\nexport function useDeferredCommit<T>(commit: (item: T) => Promise<void> | void, windowMs = 5000) {\n  const pending = useRef(new Map<string, { item: T; timer: ReturnType<typeof setTimeout> }>());\n  const run = useRef(commit);\n  run.current = commit;\n  useEffect(() => {\n    const all = pending.current;\n    return () => {\n      for (const { item, timer } of all.values()) {\n        clearTimeout(timer);\n        void run.current(item);\n      }\n      all.clear();\n    };\n  }, []);\n  const schedule = (key: string, item: T, onSettled?: (error?: unknown) => void) => {\n    const timer = setTimeout(async () => {\n      pending.current.delete(key);\n      try {\n        await run.current(item);\n        onSettled?.();\n      } catch (e) {\n        onSettled?.(e);\n      }\n    }, windowMs);\n    pending.current.set(key, { item, timer });\n  };\n  const cancel = (key: string) => {\n    const p = pending.current.get(key);\n    if (!p) return false;\n    clearTimeout(p.timer);\n    pending.current.delete(key);\n    return true;\n  };\n  return { schedule, cancel, windowMs };\n}\n"},{"path":"beamline/components/alert/alert.module.css","type":"registry:file","target":"src/ui/beamline/components/alert/alert.module.css","content":"@layer properties, theme, base, components, utilities;\n/* the tone's tint, registered so a change of tone blends instead of switching */\n@property --pui-alert-tint {\n  syntax: \"<color>\";\n  inherits: false;\n  initial-value: transparent;\n}\n@layer components {\n  /* An alert is a surface like any other, so every look frames it (blueprint's rule, pop's ink outline, ink's pen,\n     mono's dashes, metal's pocket). The tone is a tint composed over the role's own fill, never a replacement, plus\n     the icon in the tone's colour and the words: colour is never the only signal. */\n  .alert {\n    display: flex;\n    gap: var(--pui-space-3);\n    padding: var(--pui-space-3);\n    border-radius: var(--pui-radius-card);\n    background: linear-gradient(var(--pui-alert-tint), var(--pui-alert-tint)), var(--m-surface-bg);\n    color: var(--pui-ink-2);\n    /* a change of tone (warning → resolved) blends the tint instead of switching it */\n    transition:\n      var(--m-transition),\n      --pui-alert-tint var(--pui-duration-standard) var(--pui-ease-default);\n  }\n  .alert[data-tone=\"info\"] {\n    --_tone: var(--pui-blue);\n  }\n  .alert[data-tone=\"success\"] {\n    --_tone: var(--pui-green);\n  }\n  .alert[data-tone=\"warning\"] {\n    --_tone: var(--pui-orange);\n  }\n  .alert[data-tone=\"danger\"] {\n    --_tone: var(--pui-red);\n  }\n  .alert[data-tone=\"neutral\"] {\n    --_tone: var(--pui-ink-3);\n  }\n  .alert:not([data-tone=\"neutral\"]) {\n    --pui-alert-tint: color-mix(in oklab, var(--_tone) 9%, transparent);\n  }\n  .icon {\n    display: grid;\n    flex: none;\n    margin-top: var(--pui-space-0-5);\n    color: var(--_tone);\n    transition: color var(--pui-duration-standard) var(--pui-ease-default);\n  }\n  /* a new tone's glyph arrives from a little smaller, so the change of meaning is seen as well as the colour */\n  .icon[data-changed] > svg {\n    animation: glyph var(--pui-duration-standard) var(--pui-ease-default) both;\n  }\n  @keyframes glyph {\n    from {\n      opacity: 0;\n      scale: calc(1 - 0.4 * var(--pui-motion-travel, 1));\n    }\n  }\n  .action {\n    margin-top: var(--pui-space-2);\n  }\n  /* a wide alert (from 36rem) keeps its action on the message's line, at the end; a narrow one puts it underneath */\n  .alert[data-wide] > .body {\n    display: grid;\n    grid-template-columns: minmax(0, 1fr) auto;\n    column-gap: var(--pui-space-4);\n    align-items: center;\n  }\n  .alert[data-wide] > .body > :not(.action) {\n    grid-column: 1;\n  }\n  .alert[data-wide] > .body > .action {\n    grid-column: 2;\n    grid-row: 1 / span var(--_rows, 1);\n    margin-top: 0;\n    justify-content: flex-end;\n  }\n  .icon > svg {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n  /* the fold: rows collapse to zero and the alert fades, then it unmounts; it arrives the same way, growing to its\n     height where it belongs (so what sits below glides down instead of jumping) */\n  .wrap {\n    display: grid;\n    grid-template-rows: 1fr;\n    transition:\n      grid-template-rows var(--pui-duration-standard) var(--pui-ease-standard),\n      opacity var(--pui-duration-standard) var(--pui-ease-standard);\n  }\n  @starting-style {\n    .wrap[data-state=\"open\"] {\n      grid-template-rows: 0fr;\n      opacity: 0;\n    }\n  }\n  .wrap[data-state=\"closed\"] {\n    grid-template-rows: 0fr;\n    opacity: 0;\n  }\n  /* clips the fold, with room for the look's outer edge and shadow (pop's lip, soft's drop) so they are not cut */\n  .clip {\n    min-height: 0;\n    margin: calc(var(--pui-space-1-5) * -1);\n    padding: var(--pui-space-1-5);\n    overflow: hidden;\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.alert {\n  composes: m-surface from global;\n}\n"},{"path":"beamline/components/alert/alert.tsx","type":"registry:file","target":"src/ui/beamline/components/alert/alert.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport type { CSSProperties, Ref } from \"react\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { AlertDefaults } from \"./defaults\";\nexport type { AlertDefaults } from \"./defaults\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { AlertCircle, AlertTriangle, CheckCircle2, Info, X } from \"lucide-react\";\nimport { Button } from \"../button/button\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { focusAfterRemoval } from \"../../lib/focus\";\nimport styles from \"./alert.module.css\";\n\nexport type AlertTone = \"info\" | \"success\" | \"warning\" | \"danger\" | \"neutral\";\n\nexport interface AlertClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the wrap part; no override by default. */\n  wrap?: string;\n  /** Classes for the clip part; no override by default. */\n  clip?: string;\n  /** Classes for the icon part; no override by default. */\n  icon?: string;\n  /** Classes for the body part; no override by default. */\n  body?: string;\n  /** Classes for the title part; no override by default. */\n  title?: string;\n  /** Classes for the description part; no override by default. */\n  description?: string;\n  /** Classes for the action part; no override by default. */\n  action?: string;\n  /** Classes for the dismiss part; no override by default. */\n  dismiss?: string;\n}\n\nexport interface AlertProps {\n  /** Semantic colour of the displayed content. Defaults to \"info\". */\n  tone?: AlertTone;\n  /** Heading shown above the content. Defaults to no heading. */\n  title?: ReactNode;\n  /** What happened and what to do. */\n  children?: ReactNode;\n  /** Buttons or links: Retry, Update card. */\n  action?: ReactNode;\n  /** Replace the tone's icon, or false for none. */\n  icon?: ReactNode | false;\n  /** Controlled visibility; omitted by default to keep visibility locally. */\n  open?: boolean;\n  /** Initial visibility when uncontrolled; defaults to true. */\n  defaultOpen?: boolean;\n  /** Reports a request to close; no callback by default. */\n  onOpenChange?: (open: boolean) => void;\n  /** Shows a close button; defaults to whether onDismiss or onOpenChange is supplied. */\n  dismissible?: boolean;\n  /** Adds a close button; the alert folds away when open turns false. */\n  onDismiss?: () => void;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: AlertClassNames;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n}\n\nconst ICON: Record<AlertTone, ReactNode> = {\n  info: <Info />,\n  success: <CheckCircle2 />,\n  warning: <AlertTriangle />,\n  danger: <AlertCircle />,\n  neutral: <Info />,\n};\n\n/** A message that stays until it is dealt with: what happened, what to do, and the action. Danger and warning are\n *  announced as they appear; a dismissed alert folds its height away. */\nexport function Alert({\n  tone: toneProp,\n  title,\n  children,\n  action,\n  icon,\n  open: openProp,\n  defaultOpen = true,\n  onOpenChange,\n  dismissible,\n  onDismiss,\n  className,\n  style,\n  classNames,\n  ref,\n}: AlertProps) {\n  const i18n = useMessages(\"alert\", englishMessages);\n\n  const scopeDefaults = useComponentDefaults<AlertDefaults>(\"alert\", [\"tone\"]);\n  const tone = toneProp ?? scopeDefaults.tone ?? \"info\";\n\n  // an older caller that only listens to onDismiss decides the removal itself, so closing is guarded and reported beside\n  const [open, setOpen] = useControllable(openProp, defaultOpen);\n  const canDismiss = dismissible ?? !!(onDismiss || onOpenChange);\n  const [shown, setShown] = useState(open);\n  useEffect(() => {\n    if (open) setShown(true);\n  }, [open]);\n  const host = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(host, ref);\n  // a wide alert keeps its action on the message's line (read once per change of that answer)\n  const wide = useWidthAs(host, (w) => w >= 576, false, shown);\n  // the glyph of a tone it changed to arrives visibly; the first one is simply there\n  const firstTone = useRef(tone);\n  if (!shown) return null;\n  return (\n    <div\n      data-slot=\"alert-wrap\"\n      data-state={open ? \"open\" : \"closed\"}\n      className={cn(styles.wrap, classNames?.wrap)}\n      onTransitionEnd={(e) => {\n        if (!open && e.target === e.currentTarget) setShown(false);\n      }}\n    >\n      <div className={cn(styles.clip, classNames?.clip)} data-slot=\"alert-clip\">\n        <div\n          ref={attach}\n          style={{ \"--_rows\": (title ? 1 : 0) + (children ? 1 : 0) || 1, ...style } as CSSProperties}\n          data-slot=\"alert\"\n          data-tone={tone}\n          data-wide={wide && action ? \"\" : undefined}\n          role={tone === \"danger\" || tone === \"warning\" ? \"alert\" : \"status\"}\n          className={cn(styles.alert, \"text-sm leading-relaxed\", classNames?.root, className)}\n        >\n          {icon !== false && (\n            <span\n              key={tone}\n              aria-hidden\n              data-slot=\"alert-icon\"\n              data-changed={tone !== firstTone.current ? \"\" : undefined}\n              className={cn(styles.icon, classNames?.icon)}\n            >\n              {icon ?? ICON[tone]}\n            </span>\n          )}\n          <div data-slot=\"alert-body\" className={cn(styles.body, \"grid min-w-0 flex-1 gap-0.5\", classNames?.body)}>\n            {title && (\n              <p data-slot=\"alert-title\" className={cn(\"font-medium text-ink\", classNames?.title)}>\n                {title}\n              </p>\n            )}\n            {children && (\n              <div data-slot=\"alert-description\" className={classNames?.description}>\n                {children}\n              </div>\n            )}\n            {action && (\n              <div data-slot=\"alert-action\" className={cn(styles.action, \"flex flex-wrap gap-2\", classNames?.action)}>\n                {action}\n              </div>\n            )}\n          </div>\n          {canDismiss && (\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              aria-label={i18n(\"dismiss\")}\n              data-slot=\"alert-dismiss\"\n              className={cn(\"-me-1.5 -mt-1 shrink-0 text-ink-3\", classNames?.dismiss)}\n              onClick={(e) => {\n                focusAfterRemoval(e.currentTarget.closest(\"[data-slot=alert-wrap]\"));\n                // Existing event-only callers still decide when removal happens; the new state API can close locally.\n                if (dismissible !== undefined || onOpenChange !== undefined) setOpen(false);\n                onOpenChange?.(false);\n                onDismiss?.();\n              }}\n            >\n              <X />\n            </Button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default Alert;\n"},{"path":"beamline/components/alert/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/alert/defaults.ts","content":"/** Pure allowed presentation defaults for Alert; no component or runtime imports. */\nexport interface AlertDefaults {\n  /** Shared tone setting; omit to inherit the surrounding scope or the component default. */\n  tone?: \"danger\" | \"success\" | \"warning\" | \"info\" | \"neutral\";\n}\n"},{"path":"beamline/components/alert/messages.ts","type":"registry:file","target":"src/ui/beamline/components/alert/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  dismiss: \"Dismiss\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/button/button-variants.ts","type":"registry:file","target":"src/ui/beamline/components/button/button-variants.ts","content":"import { cn } from \"../../lib/utils\";\nimport styles from \"./button.module.css\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"outline\" | \"ghost\" | \"danger\" | \"accent\";\n/** sm 32 · md 36 · lg 40 · touch 44 (a bottom action bar on a phone). */\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"touch\";\n/** Market side: buy fills with --pui-up, sell with --pui-down (a red-up market flips the two tokens, not the code). */\nexport type ButtonTone = \"up\" | \"down\";\n\n/** Which paint role paints each variant: filled variants are solids, the quiet ones keep their own light paint. */\nconst ROLE: Record<ButtonVariant, string | undefined> = {\n  primary: \"m-solid\",\n  accent: \"m-solid\",\n  secondary: \"m-control\",\n  outline: undefined,\n  ghost: undefined,\n  danger: undefined,\n};\n\n/** A variant's paint without the button's layout, for a group that wears one button's look (SplitButton paints the\n *  pair as one button and leaves its halves transparent). */\nexport function buttonPaint(variant: ButtonVariant = \"primary\", tone?: ButtonTone) {\n  return cn(ROLE[variant], styles[variant], tone && styles[tone]);\n}\n\n/** The classes that make any element look like a Button, for parts that render their own element. */\nexport function buttonClassName({\n  variant = \"primary\",\n  size = \"md\",\n  tone,\n  className,\n}: { variant?: ButtonVariant; size?: ButtonSize; tone?: ButtonTone; className?: string } = {}) {\n  return cn(styles.root, ROLE[variant], styles[variant], tone && styles[tone], styles[size], className);\n}\n"},{"path":"beamline/components/button/button.module.css","type":"registry:file","target":"src/ui/beamline/components/button/button.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Button. Every colour, size and duration is a token; a variant only sets the four local properties below. */\n  .root {\n    --_button-bg: transparent;\n    --_button-bg-hover: var(--pui-hover);\n    --_button-fg: var(--pui-ink);\n    --_button-edge: none;\n    --_button-height: var(--pui-control-height-md);\n    --_button-pad: 14px;\n    position: relative;\n    isolation: isolate;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    gap: var(--pui-space-1-5);\n    flex-shrink: 0;\n    height: var(--_button-height);\n    min-width: var(--_button-height);\n    /* never wider than its room: in a narrow card the label ends in an ellipsis instead of spilling (the full words\n       stay the accessible name) */\n    max-width: 100%;\n    padding-inline: var(--_button-pad);\n    border: 0;\n    /* Nested context: inside a frame that sets --pui-nest-radius and --pui-nest-inset (and --pui-nest-ceiling for pills)\n       the corner is the shared nested corner (.nest-corner, utilities.css: radius − inset, or the button's own radius\n       where that would fall under the chip radius); elsewhere the control radius. --pui-button-radius wins over both. */\n    border-radius: var(--pui-button-radius, var(--pui-nested, var(--pui-radius-control)));\n    color: var(--_button-fg);\n    font-family: var(--pui-font-body);\n    font-size: var(--pui-text-sm);\n    font-weight: var(--pui-weight-medium);\n    line-height: 1;\n    letter-spacing: var(--pui-tracking-body);\n    white-space: nowrap;\n    text-decoration: none;\n    cursor: pointer;\n    user-select: none;\n    -webkit-tap-highlight-color: transparent;\n    /* frequency law: hover arrives at 0ms (below), leaves over the ruled departure; opacity and translate are listed\n       so a button that waits for attention (.reveal) fades away like every reveal instead of vanishing */\n    transition:\n      var(--m-transition),\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color 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  /* the quiet variants paint themselves; filled and secondary variants are painted by their look role (m-solid,\n   m-control), which these zero-specificity rules can never override */\n  :where(.root) {\n    background: var(--_button-bg);\n    box-shadow: var(--_button-edge);\n  }\n  :where(.root:hover:not(:disabled, [aria-disabled=\"true\"])) {\n    background: var(--_button-bg-hover);\n  }\n  .root:hover:not(:disabled, [aria-disabled=\"true\"]) {\n    transition-duration: 0ms;\n  }\n  /* the asChild form has no motion: a CSS press instead */\n  .root:not(button):active {\n    transform: scale(0.97);\n  }\n  .root:disabled,\n  .root[aria-disabled=\"true\"]:not([data-loading]) {\n    opacity: var(--pui-opacity-disabled);\n    cursor: not-allowed;\n  }\n  .root[data-loading] {\n    cursor: progress;\n  }\n\n  /* Icons: 16px unless the icon says otherwise (zero specificity, so size-5 or an inline size wins) */\n  :where(.root) :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n    flex-shrink: 0;\n  }\n\n  .sm {\n    --_button-height: var(--pui-control-height-sm);\n    --_button-pad: var(--pui-space-2-5);\n    /* the base gap (--pui-space-1-5); 5px had no scale step, and 6px also lets the loader's negative margin cancel\n       the gap exactly at every size */\n  }\n  .md {\n    --_button-height: var(--pui-control-height-md);\n    --_button-pad: 14px;\n  }\n  .lg {\n    --_button-height: var(--pui-control-height-lg);\n    --_button-pad: var(--pui-space-4);\n    font-size: var(--pui-text-base);\n  }\n  /* a bottom action bar on a phone: the 44 px touch target (a platform minimum, not a spacing step, so it holds when\n     the spacing parameter changes — O1 §14.2), the large label */\n  .touch {\n    --_button-height: calc(var(--pui-control-height-lg) + 0.25rem);\n    --_button-pad: var(--pui-space-5);\n    font-size: var(--pui-text-base);\n  }\n  .root[data-icon-only] {\n    padding-inline: 0;\n    width: var(--_button-height);\n  }\n\n  /* the main action: ink on the page, inverted per theme */\n  .primary {\n    --m-color: var(--pui-ink);\n    --_button-fg: var(--pui-ink-inverse);\n  }\n  .secondary {\n    --_button-fg: var(--pui-ink);\n  }\n  .outline {\n    --_button-edge: inset 0 0 0 1px var(--pui-line-strong);\n    --_button-bg-hover: var(--pui-hover);\n  }\n  .ghost {\n    --_button-fg: var(--pui-ink-2);\n    --_button-bg-hover: var(--pui-hover);\n  }\n  .ghost:hover:not(:disabled) {\n    color: var(--pui-ink);\n  }\n  .danger {\n    --_button-fg: var(--pui-red);\n    --_button-edge: inset 0 0 0 1px color-mix(in oklab, var(--pui-red) 42%, transparent);\n    --_button-bg-hover: var(--pui-red-tint);\n  }\n  .accent {\n    --m-color: var(--pui-accent);\n    --_button-fg: var(--pui-accent-foreground);\n  }\n\n  /* Market side. Declared after the variants so it wins: a filled variant takes the side's colour and its on-colour\n     ink; a quiet variant keeps its paint and colours the label. */\n  .up {\n    --m-color: var(--pui-up);\n    --_button-fg: var(--pui-up);\n  }\n  .down {\n    --m-color: var(--pui-down);\n    --_button-fg: var(--pui-down);\n  }\n  .ghost:is(.up, .down):hover:not(:disabled) {\n    color: var(--_button-fg);\n  }\n  .primary.up,\n  .accent.up {\n    --_button-fg: var(--pui-on-up);\n  }\n  .primary.down,\n  .accent.down {\n    --_button-fg: var(--pui-on-down);\n  }\n\n  /* The main actions' light, where the look provides one, as a spectrum. Glow spills a soft light in the brand's\n     spectrum from under the button's bottom edge (clipped at it, so it never tints the face) that brightens under the\n     pointer. Prism keeps it inside the glass (--m-solid-glow-fit: 1): the face the light leaves by, the bottom edge,\n     splits it into a crisp line with a faint wash rising into the face, only while pointed at. Every other look\n     leaves --m-solid-glow at none, so the layer does not exist. */\n  .primary::after,\n  .accent::after {\n    --_fit: var(--m-solid-glow-fit, 0);\n    content: var(--m-solid-glow);\n    position: absolute;\n    z-index: -1;\n    top: calc((1 - var(--_fit)) * (100% + var(--m-solid-glow-gap, 0px)));\n    inset-inline: var(--m-solid-glow-inset, 14%);\n    height: var(--m-solid-glow-height, 14px);\n    border-radius: calc(\n      var(--_fit) * var(--pui-button-radius, var(--pui-nested, var(--pui-radius-control))) + (1 - var(--_fit)) *\n        var(--pui-radius-pill)\n    );\n    background: var(--m-spectrum);\n    background-size: var(--m-spectrum-size, 100% 100%);\n    /* the part of the spectrum that shows: this button's own angle (--m-prism-seed, new at every press), turned by\n       where the pointer is across the face (--m-prism-x) in a look that turns it (--m-solid-glow-turn) */\n    background-position: calc((var(--m-prism-seed, 0.3) + var(--m-prism-x, 0.5) * var(--m-solid-glow-turn, 0)) * 100%) 0;\n    filter: blur(var(--m-solid-glow-blur, 9px));\n    clip-path: var(--m-solid-glow-clip, inset(0 -32px -32px -32px));\n    mask: var(--m-solid-glow-mask, none);\n    mask-position:\n      0 0,\n      calc(var(--m-prism-seed, 0.3) * 100%) 0;\n    mask-composite: intersect;\n    opacity: var(--m-solid-glow-rest, 0.75);\n    scale: var(--m-solid-glow-scale, 1) 1;\n    pointer-events: none;\n    transition:\n      opacity var(--pui-motion-out) var(--pui-ease-default),\n      scale var(--pui-motion-out) var(--pui-ease-default),\n      background-position var(--pui-duration-considered) var(--pui-ease-default),\n      mask-position var(--pui-duration-considered) var(--pui-ease-default);\n  }\n  /* pointed at: the light brightens (glow) or grows out from the centre and drifts along while the pointer stays\n     (prism) */\n  .primary:hover::after,\n  .accent:hover::after {\n    opacity: 1;\n    scale: 1 1;\n    transition-duration: var(--m-solid-glow-in, 0ms);\n    animation: var(--m-solid-glow-flow, none);\n  }\n  /* A change of word or colour (Save → Saved, a tone turning green) passes a band of split light across the face once,\n     in a look that has one (prism: --m-solid-shift-light; the pen hook's ::before is free on a solid otherwise). */\n  .primary[data-shift]::before,\n  .accent[data-shift]::before {\n    content: var(--m-solid-shift-light, none);\n    position: absolute;\n    inset: 0;\n    z-index: -1;\n    border: 0;\n    border-radius: inherit;\n    background: var(--m-spectrum) 0 0 / var(--m-spectrum-size, 100% 100%);\n    mask: linear-gradient(105deg, transparent 39%, #000 47.5%, #000 52.5%, transparent 61%) 0 0 / 300% 100% no-repeat;\n    opacity: 0;\n    animation: prism-shift calc(var(--pui-duration-considered) * 2.2) var(--pui-ease-default);\n    pointer-events: none;\n  }\n  @keyframes prism-shift {\n    from {\n      mask-position: 100% 0;\n      opacity: 0.25;\n    }\n    35% {\n      opacity: 0.55;\n    }\n    to {\n      mask-position: 0 0;\n      opacity: 0;\n    }\n  }\n  @media (prefers-reduced-motion: reduce) {\n    .primary::after,\n    .accent::after {\n      transition: none;\n      animation: none !important;\n    }\n    .primary[data-shift]::before,\n    .accent[data-shift]::before {\n      content: none;\n    }\n  }\n  .root:disabled::after,\n  .root[aria-disabled=\"true\"]::after {\n    content: none;\n  }\n\n  /* The loader opens its own width, and the negative margin cancels the gap so the label does not jump */\n  .loader {\n    display: inline-flex;\n    overflow: hidden;\n    margin-inline-end: calc(var(--pui-space-1-5) * -1);\n  }\n  .loader > * {\n    margin-inline-end: var(--pui-space-1-5);\n  }\n  .spinner {\n    color: currentColor;\n  }\n  .root[data-icon-only] .loader {\n    margin-inline-end: 0;\n  }\n  .root[data-icon-only] .loader > * {\n    margin-inline-end: 0;\n  }\n\n  /* The label holds its place while the text inside swaps; the leaving text pops out of flow against it */\n  .label {\n    position: relative;\n    display: inline-flex;\n    justify-content: center;\n    min-width: 0;\n    overflow: visible;\n  }\n  /* the clip box is let out by a little over a line so descenders are never cut, without changing the layout */\n  .text {\n    display: inline-block;\n    max-width: 100%;\n    margin-block: -0.2em;\n    padding-block: 0.2em;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .label[data-morphing] > .text {\n    overflow: visible;\n    text-overflow: clip;\n  }\n}\n\n@layer components {\n  :global(.pui[data-motion=\"reduced\"]) .root:not(button):active,\n  :global(.pui[data-motion=\"none\"]) .root:not(button):active {\n    transform: none;\n  }\n  @media (prefers-reduced-motion: reduce) {\n    .root:not(button):active {\n      transform: none;\n    }\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.root {\n  composes: nest-corner from global;\n}\n"},{"path":"beamline/components/button/button.tsx","type":"registry:file","target":"src/ui/beamline/components/button/button.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport {\n  Children,\n  Fragment,\n  isValidElement,\n  useLayoutEffect,\n  useRef,\n  type ButtonHTMLAttributes,\n  type PointerEvent,\n  type ReactNode,\n  type Ref,\n  type RefObject,\n  type ComponentProps,\n} from \"react\";\nimport { AnimatePresence, LazyMotion, m, spring, useIsPresent } from \"motion/react\";\nimport { animate } from \"motion/mini\";\nimport * as Slot from \"@radix-ui/react-slot\";\nimport { Spinner } from \"../spinner/spinner\";\nimport type { BuiltInLook as Look } from \"../../runtime/preset\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./button.module.css\";\n\nexport type { ButtonVariant, ButtonSize, ButtonTone } from \"./button-variants\";\nexport { buttonPaint, buttonClassName } from \"./button-variants\";\nimport { buttonClassName, type ButtonVariant, type ButtonSize, type ButtonTone } from \"./button-variants\";\nimport type { ButtonDefaults } from \"./defaults\";\nexport type { ButtonDefaults } from \"./defaults\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\n\ntype NativeProps = Omit<\n  ButtonHTMLAttributes<HTMLButtonElement>,\n  \"onDrag\" | \"onDragEnd\" | \"onDragStart\" | \"onAnimationStart\"\n>;\n\nexport interface ButtonProps extends NativeProps {\n  /** Visual weight: primary (ink-filled), secondary, outline, ghost, danger, or accent (filled with the brand). */\n  variant?: ButtonVariant;\n  /** Height 32 / 36 / 40 px; touch 44 px for a bottom action bar on a phone. */\n  size?: ButtonSize;\n  /** Market side. On primary/accent the fill becomes --pui-up (buy) or --pui-down (sell); on the quiet variants the\n   *  label takes that colour. Pair it with a word or arrow: colour is never the only signal. */\n  tone?: ButtonTone;\n  /** Shows a loader in front of the label, sets aria-busy and ignores clicks; size and focus stay. */\n  loading?: boolean;\n  classNames?: Partial<Record<\"root\" | \"label\" | \"loader\", string>>;\n  /** Paint this button in another look than the one in force around it (DESIGN-LANGUAGE.md \"Looks\"). */\n  look?: Look;\n  /** Render the single child (a link, a Radix trigger) with the button's look instead of a <button>. */\n  asChild?: boolean;\n  /** A square button holding only an icon (give it an aria-label). Decided by the children when left out: icon-only when\n   *  no words sit anywhere among them (an icon, or a frame of icons), so `<span>Close</span>` is a label. */\n  iconOnly?: boolean;\n  ref?: Ref<HTMLButtonElement>;\n}\n\n// Motion's animation features arrive in a deferred chunk (AT11): the button renders and works at once, and its press,\n// loader and label motion run once they land, a frame or two after mount. Buttons that mount after that get the\n// loaded features directly, so they render once instead of again when a lazy load resolves.\ntype Features = typeof import(\"./motion-features\").default;\nlet loaded: Features | null = null;\nconst features = () =>\n  import(\"./motion-features\").then((module) => {\n    loaded = module.default;\n    return loaded;\n  });\n\n/** Children with fragments opened up, so `{busy ? <><Icon />Saving</> : …}` reads as icon plus text. */\nconst flat = (children: ReactNode): ReactNode[] =>\n  Children.toArray(children).flatMap((c) =>\n    isValidElement<{ children?: ReactNode }>(c) && c.type === Fragment ? flat(c.props.children) : [c],\n  );\n\n/** Whether any words sit anywhere inside (an icon, or a frame of icons, has none). */\nfunction hasText(node: ReactNode): boolean {\n  if (typeof node === \"string\") return node.trim() !== \"\";\n  if (typeof node === \"number\") return true;\n  if (Array.isArray(node)) return node.some(hasText);\n  return isValidElement<{ children?: ReactNode }>(node) && hasText(node.props.children);\n}\n\n/** Splits children into the text run (the label that can change) and everything else (icons), in order, and says\n *  whether any element carries content of its own (a `<span>Close</span>` label, unlike an icon). */\nfunction split(children: ReactNode) {\n  const parts = flat(children);\n  const text = parts\n    .filter((p) => typeof p === \"string\" || typeof p === \"number\")\n    .join(\"\")\n    .trim();\n  const firstText = parts.findIndex((p) => typeof p === \"string\" || typeof p === \"number\");\n  const before = firstText === -1 ? parts : parts.slice(0, firstText).filter(isValidElement);\n  const after = firstText === -1 ? [] : parts.slice(firstText).filter(isValidElement);\n  const content = parts.some((p) => isValidElement<{ children?: ReactNode }>(p) && hasText(p.props.children));\n  return { text, before, after, content };\n}\n\n/**\n * The action control. Hover answers at once and lets go over --pui-motion-out; a press scales down at once, or swells\n * on the snappy spring where the look's --pui-press-scale is above 1 (glass), never on a trigger that anchors a\n * floating layer, which would measure a resized box; a changed label slides in while the\n * width follows on a critically damped spring; loading keeps the size and the focus.\n */\nexport function Button({\n  variant: variantProp,\n  size: sizeProp,\n  tone: toneProp,\n  loading = false,\n  look,\n  asChild = false,\n  iconOnly: iconOnlyProp,\n  className,\n  classNames,\n  children,\n  type,\n  onClick,\n  onPointerMove,\n  onPointerDown,\n  disabled,\n  ref,\n  ...props\n}: ButtonProps) {\n  const i18n = useMessages(\"button\", englishMessages);\n\n  const defaults = useComponentDefaults<ButtonDefaults>(\"button\", [\"variant\", \"size\", \"tone\"]);\n  const variant = variantProp ?? defaults.variant ?? \"primary\";\n  const size = sizeProp ?? defaults.size ?? \"md\";\n  const tone = toneProp ?? defaults.tone;\n  const host = useRef<HTMLButtonElement>(null);\n  const attach = useComposedRef(host, ref);\n  const motionTokens = useMotionTokens(host);\n  const requestedReduced = useReducedMotionSafe();\n  const reduced = requestedReduced || motionTokens.reduced;\n  // Reduced motion keeps a restrained fade; none removes it (APPEARANCE A06).\n  const fade = motionTokens.mode === \"none\" ? 0 : motionTokens.fade.in;\n  // a caller's aria-disabled keeps the button focusable (focus never drops after Save resolves) but inert to presses\n  const softDisabled = props[\"aria-disabled\"] === true || props[\"aria-disabled\"] === \"true\";\n  const { text, before, after, content } = split(children);\n  const prism = usePrismLight(host, variant === \"primary\" || variant === \"accent\", `${text}|${tone}|${variant}`);\n  // with asChild the text sits inside the child (the link), not among our own children\n  const inner = asChild && isValidElement<{ children?: ReactNode }>(children) ? split(children.props.children) : null;\n  const iconOnly = iconOnlyProp ?? (inner ? !inner.text && !inner.content : !text && !content);\n  const anchor =\n    (props[\"aria-haspopup\"] != null && props[\"aria-haspopup\"] !== false && props[\"aria-haspopup\"] !== \"false\") ||\n    \"data-state\" in props;\n  const shared = {\n    \"data-slot\": \"button\",\n    \"data-variant\": variant,\n    \"data-size\": size,\n    \"data-tone\": tone,\n    \"data-icon-only\": iconOnly ? \"\" : undefined,\n    \"data-loading\": loading ? \"\" : undefined,\n    \"data-look\": look,\n    className: buttonClassName({ variant, size, tone, className: cn(classNames?.root, className) }),\n  };\n\n  if (asChild)\n    return (\n      <Slot.Root {...shared} {...props} ref={attach}>\n        {children}\n      </Slot.Root>\n    );\n\n  const button = (\n    <m.button\n      ref={attach}\n      type={type ?? \"button\"}\n      {...shared}\n      {...props}\n      disabled={disabled}\n      aria-busy={loading || undefined}\n      aria-disabled={loading || softDisabled || undefined}\n      onPointerMove={(e) => {\n        prism.turn(e);\n        onPointerMove?.(e);\n      }}\n      onPointerDown={(e) => {\n        prism.press();\n        onPointerDown?.(e);\n      }}\n      onClick={(e) => {\n        if (loading || softDisabled) {\n          e.preventDefault();\n          return;\n        }\n        onClick?.(e);\n      }}\n      whileTap={\n        reduced || anchor || disabled || loading || softDisabled\n          ? undefined\n          : {\n              scale: motionTokens.pressScale,\n              // a press that sinks lands at once; one that swells toward the finger (glass) grows on the spring\n              transition: motionTokens.pressScale > 1 ? motionTokens.spring.snappy : { duration: 0 },\n            }\n      }\n      transition={{ scale: motionTokens.spring.snappy }}\n    >\n      <AnimatePresence initial={false}>\n        {loading && (\n          <m.span\n            key=\"loader\"\n            data-slot=\"button-loader\"\n            className={cn(styles.loader, classNames?.loader)}\n            initial={reduced ? { opacity: 0 } : { width: 0, opacity: 0 }}\n            animate={{ width: \"auto\", opacity: 1 }}\n            exit={reduced ? { opacity: 0 } : { width: 0, opacity: 0 }}\n            transition={{ duration: reduced ? fade : motionTokens.duration.standard, ease: motionTokens.ease.default }}\n          >\n            <Spinner size=\"sm\" label={i18n(\"working\")} className={styles.spinner} />\n          </m.span>\n        )}\n      </AnimatePresence>\n      {iconOnly ? !loading && before : before}\n      {!iconOnly && text && (\n        <Label text={text} reduced={!!reduced} fade={fade} motionTokens={motionTokens} className={classNames?.label} />\n      )}\n      {after}\n    </m.button>\n  );\n  return <LazyMotion features={loaded ?? features}>{button}</LazyMotion>;\n}\n\n/**\n * The main action's light inside the glass, in a look that draws one (prism): its own angle from the start, turned by\n * where the pointer is across the face, a new angle at every press, and a sweep of split light (data-shift) when the\n * button's word, tone or variant changes after it first appears. Writes two style properties and one attribute on the\n * button; nothing runs at rest. Looks without this light ignore all three.\n */\nfunction usePrismLight(host: RefObject<HTMLButtonElement | null>, solid: boolean, state: string) {\n  const frame = useRef(0);\n  const shown = useRef(state);\n  const ending = useRef(0);\n  useLayoutEffect(() => {\n    const el = host.current;\n    if (el && solid) el.style.setProperty(\"--m-prism-seed\", Math.random().toFixed(3));\n  }, [host, solid]);\n  useLayoutEffect(() => {\n    const el = host.current;\n    if (!el || !solid || shown.current === state) return;\n    shown.current = state;\n    // restart the sweep if it is still running\n    delete el.dataset.shift;\n    void el.offsetWidth;\n    el.dataset.shift = \"\";\n    clearTimeout(ending.current);\n    ending.current = window.setTimeout(() => delete el.dataset.shift, 1200);\n  }, [host, solid, state]);\n  useLayoutEffect(\n    () => () => {\n      clearTimeout(ending.current);\n      cancelAnimationFrame(frame.current);\n    },\n    [],\n  );\n  return {\n    turn: (e: PointerEvent<HTMLButtonElement>) => {\n      if (!solid || e.pointerType !== \"mouse\" || frame.current) return;\n      const el = e.currentTarget;\n      const x = e.clientX;\n      frame.current = requestAnimationFrame(() => {\n        frame.current = 0;\n        const r = el.getBoundingClientRect();\n        if (r.width) el.style.setProperty(\"--m-prism-x\", ((x - r.left) / r.width).toFixed(3));\n      });\n    },\n    press: () => {\n      if (solid) host.current?.style.setProperty(\"--m-prism-seed\", Math.random().toFixed(3));\n    },\n  };\n}\n\n/** The text run. When it changes, the old words leave upward and the new ones rise in, while the width moves from the\n *  old measure to the new one; no measuring happens while the label is unchanged. */\nfunction Label({\n  text,\n  reduced,\n  fade,\n  motionTokens,\n  className,\n}: {\n  text: string;\n  reduced: boolean;\n  /** Seconds of the label crossfade under reduced motion (0 under none). */\n  fade: number;\n  /** The button's own, resolved at the button: the label sits inside it, in the same look and motion. */\n  motionTokens: ReturnType<typeof useMotionTokens>;\n  className?: string;\n}) {\n  const slot = useRef<HTMLSpanElement>(null);\n  const latest = useRef(motionTokens);\n  useLayoutEffect(() => {\n    latest.current = motionTokens;\n  }, [motionTokens]);\n  const last = useRef<{ text: string; width: number } | null>(null);\n  useLayoutEffect(() => {\n    const el = slot.current;\n    if (!el) return;\n    const prev = last.current;\n    // first mount: read the width after paint, when layout is already clean, instead of forcing a layout per button\n    if (!prev) {\n      last.current = { text, width: Number.NaN };\n      const id = requestAnimationFrame(() => {\n        if (last.current?.text === text) last.current = { text, width: el.getBoundingClientRect().width };\n      });\n      return () => cancelAnimationFrame(id);\n    }\n    el.style.width = \"\";\n    const width = el.getBoundingClientRect().width;\n    last.current = { text, width };\n    // a label that is not rendered (a hidden toolbar on a phone) has nothing to animate\n    if (prev.text === text || reduced || Number.isNaN(prev.width) || !el.getClientRects().length) return;\n    // The same spring, played natively: motion/mini samples the generator into a linear() easing.\n    const { visualDuration, bounce } = latest.current.spring.morph;\n    // while the width eases the words are clipped, not cut short with an ellipsis (that is for a room too narrow)\n    el.dataset.morphing = \"\";\n    const run = animate(el, { width: [`${prev.width}px`, `${width}px`] }, { type: spring, visualDuration, bounce });\n    run.then(() => {\n      el.style.width = \"\";\n      delete el.dataset.morphing;\n    });\n    // cancel, not stop: stop commits the running value, which throws once the element stops being rendered, and the\n    // next change measures afresh anyway\n    return () => {\n      run.cancel();\n      el.style.width = \"\";\n      delete el.dataset.morphing;\n    };\n  }, [text, reduced]);\n  // A label that changes faster than its words can rise in (a count read live while a hand drags) shows each new\n  // value at once: otherwise every value would start invisible and the button would read blank for the whole drag.\n  const rapid = useRapid(text, motionTokens.duration.standard * 1000);\n  const enter = reduced ? { opacity: 0 } : { opacity: 0, y: \"0.45em\", filter: `blur(${motionTokens.blur.subtle}px)` };\n  const leave = reduced\n    ? { opacity: 0 }\n    : {\n        opacity: 0,\n        y: \"-0.45em\",\n        filter: `blur(${motionTokens.blur.subtle}px)`,\n        transition: { duration: motionTokens.duration.fast },\n      };\n  return (\n    <span ref={slot} data-slot=\"button-label\" className={cn(styles.label, className)}>\n      <AnimatePresence mode=\"popLayout\" initial={false} custom={rapid}>\n        <Words\n          key={text}\n          className={styles.text}\n          custom={rapid}\n          variants={{\n            enter: (fast: boolean) => (fast ? { opacity: 1, y: \"0em\", filter: \"blur(0px)\" } : enter),\n            shown: { opacity: 1, y: \"0em\", filter: \"blur(0px)\" },\n            leave: (fast: boolean) => (fast ? { opacity: 0, transition: { duration: 0 } } : leave),\n          }}\n          initial=\"enter\"\n          animate=\"shown\"\n          exit=\"leave\"\n          transition={{ duration: reduced ? fade : motionTokens.duration.standard, ease: motionTokens.ease.default }}\n        >\n          {text}\n        </Words>\n      </AnimatePresence>\n    </span>\n  );\n}\n\n/** Whether `text` changed again within `ms` of its last change. Read in render: the answer belongs to this text. */\nfunction useRapid(text: string, ms: number) {\n  const seen = useRef({ text, at: Number.NEGATIVE_INFINITY, rapid: false });\n  if (seen.current.text !== text) {\n    const at = performance.now();\n    seen.current = { text, at, rapid: at - seen.current.at < ms };\n  }\n  return seen.current.rapid;\n}\n\n/** One label's words. Leaving, they are already gone for assistive tech: the button's name is the new label at once,\n *  not both while the old words animate away (or for good, where time stands still in a test). */\nfunction Words(props: ComponentProps<typeof m.span>) {\n  const present = useIsPresent();\n  return <m.span {...props} aria-hidden={present ? undefined : true} />;\n}\n\nexport default Button;\n"},{"path":"beamline/components/button/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/button/defaults.ts","content":"import type { ButtonSize, ButtonTone, ButtonVariant } from \"./button-variants\";\n/** Scoped design choices only; no loading, events or domain state. */\nexport interface ButtonDefaults {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  tone?: ButtonTone;\n}\n"},{"path":"beamline/components/button/messages.ts","type":"registry:file","target":"src/ui/beamline/components/button/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  working: \"Working\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/button/motion-features.ts","type":"registry:file","target":"src/ui/beamline/components/button/motion-features.ts","content":"// REASON: Motion's DOM animation features (tween, spring, presence, press) are loaded after the first paint, so a\n// Button costs a small part of Motion on first load; LazyMotion in button.tsx renders until they arrive.\nimport { domAnimation } from \"motion/react\";\n\nexport default domAnimation;\n"},{"path":"beamline/components/dropdown-menu/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/dropdown-menu/defaults.ts","content":"/** Pure allowed presentation defaults for DropdownMenu; no component or runtime imports. */\nexport interface DropdownMenuDefaults {\n  /** Shared variant setting; omit to inherit the surrounding scope or the component default. */\n  variant?: \"primary\" | \"secondary\" | \"outline\" | \"ghost\" | \"danger\" | \"accent\";\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\" | \"touch\";\n  /** Shared align setting; omit to inherit the surrounding scope or the component default. */\n  align?: \"start\" | \"center\" | \"end\";\n}\n"},{"path":"beamline/components/dropdown-menu/dropdown-menu.tsx","type":"registry:file","target":"src/ui/beamline/components/dropdown-menu/dropdown-menu.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { DropdownMenuDefaults } from \"./defaults\";\nexport type { DropdownMenuDefaults } from \"./defaults\";\n\nimport type { CSSProperties, Ref, ReactElement, ReactNode } from \"react\";\nimport * as RadixMenu from \"@radix-ui/react-dropdown-menu\";\nimport { Button, type ButtonSize, type ButtonVariant } from \"../button/button\";\nimport {\n  MenuItems,\n  MenuSurface,\n  useMenuDirection,\n  type MenuItem,\n  type MenuClassNames,\n} from \"../_shared/menu\";\nimport { cn } from \"../../lib/utils\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\n\nexport type { MenuItem, MenuClassNames };\n\nexport interface DropdownMenuProps {\n  /** The menu's name: the trigger's text, or its accessible name when the trigger is an icon. */\n  label: string;\n  /** Items rendered in the component. Required. */\n  items: MenuItem[];\n  /** An icon for the built-in trigger (MoreHorizontal for row actions); with it the trigger shows only the icon, and\n   *  label becomes its accessible name, unless showLabel is set. */\n  icon?: ReactNode;\n  /** Shows the label beside the icon. Defaults to false. */\n  showLabel?: boolean;\n  /** Your own trigger (a Button, an avatar); it receives the open state and keyboard handling. */\n  trigger?: ReactElement;\n  /** Visual weight of the control. Defaults to \"ghost\". */\n  variant?: ButtonVariant;\n  /** Control size within the supported size steps. Defaults to \"sm\". */\n  size?: ButtonSize;\n  /** Alignment within the available space. Defaults to \"end\". */\n  align?: \"start\" | \"center\" | \"end\";\n  /** Edge from which the panel or anchored content opens. Defaults to \"bottom\". */\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  /** Controlled open state; omit to use defaultOpen. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. Defaults to false. */\n  defaultOpen?: boolean;\n  /** Reports open state after a change; no callback by default. */\n  onOpenChange?: (open: boolean) => void;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: MenuClassNames;\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  /** Reading direction; inherits the surrounding direction when omitted. Defaults to the surrounding reading direction. */\n  dir?: \"ltr\" | \"rtl\";\n}\n\n/**\n * A list of actions behind a trigger: row actions, \"More\", an account menu. One highlight plate glides between rows\n * under the pointer and jumps with the keyboard; type to jump to an item; → opens a submenu.\n */\nexport function DropdownMenu({\n  label,\n  items,\n  icon,\n  showLabel = false,\n  trigger,\n  variant: variantProp,\n  size: sizeProp,\n  align: alignProp,\n  side = \"bottom\",\n  open,\n  defaultOpen,\n  onOpenChange,\n  className,\n  classNames,\n  style,\n  ref,\n  dir: direction,\n}: DropdownMenuProps) {\n  const scopeDefaults = useComponentDefaults<DropdownMenuDefaults>(\"dropdown-menu\", [\"variant\", \"size\", \"align\"]);\n  const variant = variantProp ?? scopeDefaults.variant ?? \"ghost\";\n  const size = sizeProp ?? scopeDefaults.size ?? \"sm\";\n  const align = alignProp ?? scopeDefaults.align ?? \"end\";\n\n  const container = usePortalContainer();\n  const iconOnly = !!icon && !showLabel;\n  const { dir, triggerProps } = useMenuDirection(direction);\n  return (\n    <RadixMenu.Root dir={dir} open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>\n      <RadixMenu.Trigger asChild data-slot=\"dropdown-menu-trigger\" className={classNames?.trigger} {...triggerProps}>\n        {trigger ?? (\n          <Button\n            data-slot=\"dropdown-menu-trigger\"\n            variant={variant}\n            size={size}\n            aria-label={iconOnly ? label : undefined}\n          >\n            {icon}\n            {!iconOnly && label}\n          </Button>\n        )}\n      </RadixMenu.Trigger>\n      {container !== null && (\n        <RadixMenu.Portal container={container}>\n          <MenuSurface\n            kit={RadixMenu}\n            label={label}\n            align={align}\n            side={side}\n            sideOffset={6}\n            className={cn(classNames?.root, className)}\n            classNames={classNames}\n            style={style}\n            ref={ref}\n          >\n            <MenuItems kit={RadixMenu} items={items} container={container} classNames={classNames} />\n          </MenuSurface>\n        </RadixMenu.Portal>\n      )}\n    </RadixMenu.Root>\n  );\n}\n\nexport default DropdownMenu;\n"},{"path":"beamline/components/empty-state/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/empty-state/defaults.ts","content":"/** Pure allowed presentation defaults for EmptyState; no component or runtime imports. */\nexport interface EmptyStateDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\n"},{"path":"beamline/components/empty-state/empty-state.module.css","type":"registry:file","target":"src/ui/beamline/components/empty-state/empty-state.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* An empty state answers to its room (data-room, read from its own width once per change: it is often centred in a\n     panel, where a size container would have no width of its own). Its air, icon ring and actions step with that room;\n     a caller's own padding or width classes still win (they sit in the utilities layer above this one). It arrives with\n     a short fade, as it usually replaces a list that just emptied. */\n  .root {\n    display: grid;\n    justify-items: center;\n    text-align: center;\n    animation: arrive var(--pui-fade-in) var(--pui-ease-default) both;\n  }\n  @keyframes arrive {\n    from {\n      opacity: 0;\n    }\n  }\n  .root[data-size=\"md\"] {\n    gap: var(--pui-space-3);\n    padding: var(--pui-space-10) var(--pui-space-6);\n  }\n  .root[data-size=\"sm\"] {\n    gap: var(--pui-space-2);\n    padding: var(--pui-space-6) var(--pui-space-4);\n  }\n  /* a big room (a whole page or panel body) gets more air above and below */\n  .root[data-size=\"md\"][data-room=\"wide\"] {\n    padding-block: var(--pui-space-16);\n  }\n  /* a narrow room (a sidebar, a phone column, a small tile): less air, the dashed ring folds away, and the actions\n     stack full width so each is an easy target */\n  .root[data-room=\"narrow\"] {\n    gap: var(--pui-space-2);\n    padding: var(--pui-space-5) var(--pui-space-3);\n  }\n  .root[data-room=\"narrow\"] .ring {\n    display: none;\n  }\n  .root[data-room=\"narrow\"] > .actions {\n    flex-direction: column;\n    justify-self: stretch;\n  }\n  .root[data-room=\"narrow\"] > .actions > * {\n    width: 100%;\n  }\n\n  /* layout row: a strip for a list body or a table section — icon, words and actions in one line from 32rem, the\n     stack below it */\n  .root[data-row] {\n    grid-template-columns: auto minmax(0, 1fr) auto;\n    align-items: center;\n    justify-items: start;\n    column-gap: var(--pui-space-4);\n    text-align: start;\n  }\n  .root[data-row] > .icon {\n    margin: 0 var(--pui-space-2);\n  }\n  .root[data-row] > .actions {\n    margin-top: 0;\n    justify-content: flex-end;\n  }\n\n  .icon {\n    position: relative;\n    display: grid;\n    place-items: center;\n    border-radius: var(--pui-radius-panel);\n    color: var(--pui-ink-2);\n  }\n  .root[data-size=\"md\"] > .icon {\n    width: var(--pui-space-12);\n    height: var(--pui-space-12);\n    margin-bottom: var(--pui-space-1);\n  }\n  .root[data-size=\"sm\"] > .icon {\n    width: calc(var(--pui-space-1) * 9);\n    height: calc(var(--pui-space-1) * 9);\n  }\n  .icon > svg {\n    width: var(--pui-icon-lg);\n    height: var(--pui-icon-lg);\n  }\n  .root[data-size=\"sm\"] > .icon > svg {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n  /* the dashed ring around the icon says \"a place waiting to be filled\"; it firms up while the next step is pointed at\n     or focused, tying the picture to the action */\n  .ring {\n    position: absolute;\n    inset: calc(var(--pui-space-2) * -1);\n    border: 1px dashed var(--pui-line);\n    border-radius: var(--pui-radius-surface);\n    transition: border-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root:has(> .actions :is(:hover, :focus-visible)) .ring {\n    border-color: var(--pui-line-strong);\n    transition-duration: 0ms;\n  }\n  .body {\n    display: grid;\n    gap: var(--pui-space-1);\n    max-width: 44ch;\n  }\n  .actions {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    justify-content: center;\n    gap: var(--pui-space-2);\n    margin-top: var(--pui-space-1);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.icon {\n  composes: m-control from global;\n}\n"},{"path":"beamline/components/empty-state/empty-state.tsx","type":"registry:file","target":"src/ui/beamline/components/empty-state/empty-state.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { EmptyStateDefaults } from \"./defaults\";\nexport type { EmptyStateDefaults } from \"./defaults\";\n\nimport { useRef, type CSSProperties, type Ref, type ReactNode } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./empty-state.module.css\";\n\nexport interface EmptyStateClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the icon part; no override by default. */\n  icon?: string;\n  /** Classes for the ring part; no override by default. */\n  ring?: string;\n  /** Classes for the body part; no override by default. */\n  body?: string;\n  /** Classes for the title part; no override by default. */\n  title?: string;\n  /** Classes for the description part; no override by default. */\n  description?: string;\n  /** Classes for the actions part; no override by default. */\n  actions?: string;\n}\n\nexport interface EmptyStateProps {\n  /** Say what will be here: \"No reports yet\". */\n  title: ReactNode;\n  /** Why it is empty and what happens next. */\n  description?: ReactNode;\n  /** The next step: create, import, invite. */\n  action?: ReactNode;\n  /** A quieter second step: docs, a sample. */\n  secondary?: ReactNode;\n  /** Icon element shown beside the label. Defaults to no leading icon. */\n  icon?: ReactNode;\n  /** compact for panels and table bodies. */\n  size?: \"md\" | \"sm\";\n  /** stack (default): centred. row: a strip for a list body or table section — icon, words and actions in one line\n   *  from 32rem, the stack below. */\n  layout?: \"stack\" | \"row\";\n  /** The region's name, when the title is not text. */\n  label?: string;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: EmptyStateClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLElement>;\n}\n\n/** What a place will hold and the first step to fill it, instead of a blank. For filters with no results, say which\n *  filter to loosen. */\nexport function EmptyState({\n  title,\n  description,\n  action,\n  secondary,\n  icon,\n  size: sizeProp,\n  layout = \"stack\",\n  label,\n  className,\n  classNames,\n  style,\n  ref,\n}: EmptyStateProps) {\n  const scopeDefaults = useComponentDefaults<EmptyStateDefaults>(\"empty-state\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const host = useRef<HTMLElement>(null);\n  const attach = useComposedRef(host, ref);\n  // the room, read once per change of step: narrow below 16rem, wide from 42rem, a row (for layout row) from 32rem of\n  // content at the default padding. Read on the outer width: the padding steps with the room, so the content width\n  // would flip it back and forth every frame when it sits near a step.\n  const pad = size === \"sm\" ? 32 : 48;\n  const room = useWidthAs(\n    host,\n    (w) => {\n      const s = host.current ? getComputedStyle(host.current) : null;\n      const outer = w + (s ? parseFloat(s.paddingLeft) + parseFloat(s.paddingRight) : pad) - pad;\n      return outer < 256 ? \"narrow\" : outer >= 672 ? \"wide\" : outer >= 512 ? \"row\" : \"default\";\n    },\n    \"default\",\n  );\n\n  return (\n    <section\n      ref={attach}\n      style={style}\n      data-slot=\"empty-state\"\n      data-size={size}\n      data-room={room === \"row\" ? \"default\" : room}\n      data-row={layout === \"row\" && (room === \"row\" || room === \"wide\") ? \"\" : undefined}\n      aria-label={label ?? (typeof title === \"string\" ? title : undefined)}\n      className={cn(styles.root, classNames?.root, className)}\n    >\n      {icon && (\n        <span data-slot=\"empty-state-icon\" aria-hidden className={cn(styles.icon, classNames?.icon)}>\n          <span data-slot=\"empty-state-ring\" className={cn(styles.ring, classNames?.ring)} />\n          {icon}\n        </span>\n      )}\n      <div data-slot=\"empty-state-body\" className={cn(styles.body, classNames?.body)}>\n        <h3\n          data-slot=\"empty-state-title\"\n          className={cn(\"text-ink\", size === \"md\" ? \"text-subheading\" : \"text-label\", classNames?.title)}\n        >\n          {title}\n        </h3>\n        {description && (\n          <p\n            data-slot=\"empty-state-description\"\n            className={cn(\"leading-relaxed text-ink-3\", size === \"md\" ? \"text-sm\" : \"text-xs\", classNames?.description)}\n          >\n            {description}\n          </p>\n        )}\n      </div>\n      {(action || secondary) && (\n        <div data-slot=\"empty-state-actions\" className={cn(styles.actions, classNames?.actions)}>\n          {action}\n          {secondary}\n        </div>\n      )}\n    </section>\n  );\n}\n\nexport default EmptyState;\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/segmented-control/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/segmented-control/defaults.ts","content":"/** Pure allowed presentation defaults for SegmentedControl; no component or runtime imports. */\nexport interface SegmentedControlDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\n"},{"path":"beamline/components/segmented-control/messages.ts","type":"registry:file","target":"src/ui/beamline/components/segmented-control/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  chooseOne: \"Choose one\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/segmented-control/segmented-control.module.css","type":"registry:file","target":"src/ui/beamline/components/segmented-control/segmented-control.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Segmented control: a well (the inset role of the look in force) with one raised thumb (the control role) that\n   slides to the chosen segment. The segments themselves are transparent text buttons on top. */\n  .root {\n    --pui-segment-height: 32px;\n    --pui-segment-pad: 12px;\n    position: relative;\n    isolation: isolate;\n    display: inline-flex;\n    align-items: center;\n    gap: var(--pui-space-0-5);\n    /* the well's inset is size, not spacing: a control keeps its height when the Spacing parameter changes (O1 §14.2) */\n    padding: var(--pui-size-0-5, 0.125rem);\n    border-radius: var(--pui-radius-control);\n    /* never wider than its room: what does not fit folds (segmented-control.tsx tries full, compact, icons, scroll) */\n    max-width: 100%;\n    min-width: 0;\n  }\n  .fill {\n    display: flex;\n    width: 100%;\n  }\n  /* As a form field the well carries the field's 1px ring (3:1 in every look), so an unanswered choice reads as a\n     control beside the inputs, and turns red with the field's error like they do. */\n  .root[data-field] {\n    box-shadow:\n      inset 0 0 0 1px var(--m-field-ring),\n      var(--m-inset-edge, 0 0 transparent);\n  }\n  .root[data-field][aria-invalid=\"true\"] {\n    box-shadow:\n      inset 0 0 0 1px var(--pui-red),\n      var(--m-inset-edge, 0 0 transparent);\n  }\n  .sm {\n    --pui-segment-height: 28px;\n    --pui-segment-pad: 10px;\n  }\n\n  /* placed and moved by the contour engine (ui/lib/contour.ts), which sets its box and, while it travels, its outline */\n  .thumb {\n    position: absolute;\n    z-index: -1;\n    top: 0;\n    left: 0;\n    border-radius: calc(var(--pui-radius-control) - 2px);\n  }\n\n  .segment {\n    position: relative;\n    flex-shrink: 0;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    gap: var(--pui-space-1-5);\n    height: var(--pui-segment-height);\n    padding-inline: var(--pui-segment-pad);\n    border-radius: calc(var(--pui-radius-control) - 2px);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-sm);\n    font-weight: var(--pui-weight-medium);\n    white-space: nowrap;\n    transition:\n      color var(--pui-motion-out) var(--pui-ease-default),\n      scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .sm .segment {\n    font-size: var(--pui-text-xs);\n  }\n  .fill .segment {\n    flex: 1;\n  }\n  /* compact: tighter segments, and accessories (a \"New\" badge) fold first */\n  .root[data-fold=\"compact\"],\n  .root[data-fold=\"words\"],\n  .root[data-fold=\"icons\"],\n  .root[data-fold=\"scroll\"] {\n    --pui-segment-pad: var(--pui-space-2);\n  }\n  .sm[data-fold=\"compact\"],\n  .sm[data-fold=\"words\"],\n  .sm[data-fold=\"icons\"],\n  .sm[data-fold=\"scroll\"] {\n    --pui-segment-pad: var(--pui-space-1-5);\n  }\n  .root:is([data-fold=\"compact\"], [data-fold=\"words\"], [data-fold=\"icons\"], [data-fold=\"scroll\"]) .accessory {\n    display: none;\n  }\n  /* words: the icons give way before the words, which carry the meaning (an icon is the segment's one unnamed part) */\n  .root[data-fold=\"words\"] .segment > :not([data-slot]) {\n    display: none;\n  }\n  /* icons: each segment a square; its words stay as its name for assistive tech and its tooltip */\n  .root[data-fold=\"icons\"] .segment {\n    width: var(--pui-segment-height);\n    padding-inline: 0;\n  }\n  .root[data-fold=\"icons\"] .label {\n    position: absolute;\n    width: 1px;\n    height: 1px;\n    overflow: hidden;\n    clip-path: inset(50%);\n    white-space: nowrap;\n  }\n  /* scroll: the row scrolls under soft edges (no bar), and the choice is kept in view; only an edge with more past it\n     fades (data-edge, kept by segmented-control.tsx), so the row says which way the rest lies */\n  .root[data-fold=\"scroll\"] {\n    --_fade-start: 0px;\n    --_fade-end: 0px;\n    overflow-x: auto;\n    overscroll-behavior-x: contain;\n    scrollbar-width: none; /* organism-allow: paint-recipe-copy -- a state selector cannot compose */\n    mask-image: linear-gradient(\n      to right,\n      transparent,\n      currentColor var(--_fade-start),\n      currentColor calc(100% - var(--_fade-end)),\n      transparent\n    );\n  }\n  .root[data-fold=\"scroll\"][data-edge~=\"start\"] {\n    --_fade-start: var(--pui-space-6);\n  }\n  .root[data-fold=\"scroll\"][data-edge~=\"end\"] {\n    --_fade-end: var(--pui-space-6);\n  }\n  .root[data-fold=\"scroll\"]:dir(rtl) {\n    --_fade-start: 0px;\n    --_fade-end: 0px;\n  }\n  .root[data-fold=\"scroll\"]:dir(rtl)[data-edge~=\"start\"] {\n    --_fade-end: var(--pui-space-6);\n  }\n  .root[data-fold=\"scroll\"]:dir(rtl)[data-edge~=\"end\"] {\n    --_fade-start: var(--pui-space-6);\n  }\n  .root[data-fold=\"scroll\"]::-webkit-scrollbar {\n    display: none;\n  }\n  /* select (a form field at its narrowest): the select below takes over; the segments stay laid out but unseen and take\n     no room (no height, and their gap given back), so a wider room can be measured and bring them back */\n  .root[data-fold=\"select\"] {\n    height: 0;\n    min-height: 0;\n    margin-block-start: calc(var(--pui-field-gap, 0px) * -1);\n    padding-block: 0;\n    overflow: hidden;\n    visibility: hidden;\n    box-shadow: none;\n  }\n  .label,\n  .accessory {\n    display: inline-flex;\n    align-items: center;\n  }\n  .segment:hover:not(:disabled),\n  .segment[aria-checked=\"true\"] {\n    color: var(--pui-ink);\n    transition-duration: 0ms;\n  }\n  /* a press is acknowledged at once on the words; the choice (and the thumb) follows on release */\n  .segment:active:not(:disabled) {\n    scale: var(--pui-press-scale);\n    transition-duration: 0ms;\n  }\n  .segment:disabled {\n    opacity: var(--pui-opacity-disabled);\n    pointer-events: none;\n  }\n  /* the whole group off: the well and its thumb dim once, as one control, with the choice still readable */\n  .root[data-disabled] {\n    opacity: var(--pui-opacity-disabled);\n  }\n  .root[data-disabled] .segment:disabled {\n    opacity: 1;\n  }\n  .segment :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n    flex-shrink: 0;\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.root {\n  composes: m-inset from global;\n}\n.thumb {\n  composes: m-control from global;\n}\n"},{"path":"beamline/components/segmented-control/segmented-control.tsx","type":"registry:file","target":"src/ui/beamline/components/segmented-control/segmented-control.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SegmentedControlDefaults } from \"./defaults\";\nexport type { SegmentedControlDefaults } from \"./defaults\";\n\nimport {\n  Fragment,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type Ref,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useIndicator } from \"../../hooks/use-indicator\";\nimport { useContour } from \"../../hooks/use-contour\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { ChoiceField, choiceIsField, type ChoiceFieldAria } from \"../_shared/field\";\nimport { Select } from \"../select/select\";\nimport { TextMorph } from \"../text-morph/text-morph\";\nimport { Tooltip } from \"../tooltip/tooltip\";\nimport styles from \"./segmented-control.module.css\";\n\nexport interface Segment {\n  /** Value represented by this item. Required. */\n  value: string;\n  /** Text displayed for this item. Required. */\n  label: string;\n  /** Icon element displayed with this item. Omitted by default. */\n  icon?: ReactNode;\n  /** Content after the label, such as a badge. */\n  accessory?: ReactNode;\n  /** A short note beside the label in a tinted chip, such as a saving (\"−20%\"); it folds with the accessories in a\n   *  narrow room. */\n  note?: string;\n  /** The note while this segment is chosen (\"Saving 20%\"): the letters it shares with `note` stay and the rest arrive,\n   *  so the choice reads as what it gains (a billing period: Monthly · Yearly −20%). */\n  activeNote?: string;\n  /** Whether this option is unavailable for selection. Omitted by default. */\n  disabled?: boolean;\n}\n\nexport type SegmentedControlClassNames = Partial<\n  Record<\"root\" | \"thumb\" | \"segment\" | \"note\" | \"field\" | \"label\" | \"description\" | \"error\", string>\n>;\n\n/** How the segments meet a room narrower than they are: full, compact (tighter, accessories folded), words (the icons\n *  give way, the words carry the meaning), icons only (when every segment has one), then, as a form field, a select listing every choice (a view switch in a toolbar scrolls\n *  instead, keeping the choice in view). The thumb travels in every fold but the select. */\ntype Fold = \"full\" | \"compact\" | \"words\" | \"icons\" | \"scroll\" | \"select\";\n\nconst segmentFor = (root: HTMLElement | null, value: string) =>\n  [...(root?.querySelectorAll<HTMLElement>(\"[data-slot=segmented-item]\") ?? [])].find(\n    (item) => item.getAttribute(\"data-value\") === value,\n  );\n\nexport interface SegmentedControlProps {\n  /** Available choices with their labels and values. Required. */\n  options: Segment[];\n  /** Controlled value of the chosen segment; omit to use defaultValue. */\n  value?: string;\n  /** Initial value of the chosen segment when uncontrolled. Defaults to the first option value. */\n  defaultValue?: string;\n  /** Reports value of the chosen segment after an edit; no callback by default. */\n  onValueChange?: (value: string) => void;\n  /** The group's name or question (\"Range\", \"View\", \"Gender\"), shown above it like every field's label; `hideLabel` keeps it for\n   *  assistive tech only (a view switch). */\n  label: string;\n  /** Lay it out as a form field (label above, description or error below). On unless the label is hidden and there is\n   *  no description, error or required mark. */\n  field?: boolean;\n  /** Shows the label visually while keeping its accessible name; defaults to true. Takes precedence over hideLabel. */\n  showLabel?: boolean;\n  /** Hides the label; defaults to false. @deprecated Use showLabel={false} instead. */\n  hideLabel?: boolean;\n  /** One line of help under the segments (makes it a field). */\n  description?: ReactNode;\n  /** The problem and its fix, e.g. a required choice not made (makes it a field and marks the group invalid). */\n  error?: ReactNode;\n  /** Marks the field's label as required (makes it a field). */\n  required?: boolean;\n  /** Submitted with a native form as name=value. */\n  name?: string;\n  /** Prevents interaction with every segment; the choice stays shown. Defaults to false. */\n  disabled?: boolean;\n  /** Runs when a segment is pointed at or focused, before it is chosen: prefetch its data. */\n  onOptionIntent?: (value: string) => void;\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: \"sm\" | \"md\";\n  /** Stretch to the container and share the width equally. */\n  fill?: boolean;\n  /** Show each segment's icon alone, as a square; its label stays the accessible name and the tooltip. */\n  iconOnly?: boolean;\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  /** Reading direction; inherits the surrounding direction when omitted. Defaults to the surrounding reading direction. */\n  dir?: \"ltr\" | \"rtl\";\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?: SegmentedControlClassNames;\n}\n\n/**\n * Two to five views or ranges of the same content: 1D / 1W / 1M, List / Board. One raised thumb slides to the chosen\n * segment as matter (ui/lib/contour.ts, shaped by the look): it leads, rounds its front and gathers, never bouncing. It is\n * a radio group: Tab enters at the chosen one, arrows move and choose; the choice is made at once, only the thumb settles.\n */\nexport function SegmentedControl({\n  options,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  label,\n  field,\n  hideLabel: hideLabelProp,\n  showLabel,\n  description,\n  error,\n  required,\n  name,\n  disabled = false,\n  onOptionIntent,\n  size: sizeProp,\n  fill = false,\n  iconOnly = false,\n  className,\n  style,\n  dir,\n  ref,\n  classNames,\n}: SegmentedControlProps) {\n  const i18n = useMessages(\"segmented-control\", englishMessages);\n\n  useDeprecatedProp(\"SegmentedControl\", \"hideLabel\", \"showLabel={false}\", hideLabelProp !== undefined);\n  const hideLabel = showLabel === undefined ? hideLabelProp : !showLabel;\n  const scopeDefaults = useComponentDefaults<SegmentedControlDefaults>(\"segmented-control\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const [value, choose] = useControllable(valueProp, defaultValue ?? options[0]?.value, onValueChange);\n  const [root, thumb] = useIndicator<HTMLDivElement>(\"[role=radio][aria-checked=true]\", `${value}:${options.length}`);\n  const thumbRef = useContour<HTMLSpanElement>(thumb);\n  const rootRef = useComposedRef(root, ref);\n  // the room decides how much of each segment shows: every fold is tried in order on the control itself (an attribute,\n  // read before paint) and the first that fits is kept; a change of the room tries again from full, so it comes back\n  const [fold, setFold] = useState<Fold>(iconOnly ? \"icons\" : \"full\");\n  const refocus = useRef<string | undefined>(undefined);\n  const withTips = useRef(false);\n  useLayoutEffect(() => {\n    withTips.current = fold === \"icons\";\n    const el = root.current;\n    if (!el) return;\n    const v = refocus.current;\n    refocus.current = undefined;\n    if (v !== undefined) segmentFor(el, v)?.focus({ preventScroll: true });\n  }, [fold, root]);\n  const asField = choiceIsField(field, label, hideLabel, description != null || error != null || !!required);\n  const allIcons = options.length > 0 && options.every((o) => o.icon != null);\n  const someIcons = options.some((o) => o.icon != null);\n  const optionSet = options.map((o) => `${o.value}\\u0000${o.label}`).join(\"\\u0001\");\n  useLayoutEffect(() => {\n    const el = root.current;\n    if (!el) return;\n    const folds: Fold[] = iconOnly\n      ? [\"icons\"]\n      : allIcons\n        ? [\"full\", \"compact\", \"words\", \"icons\"]\n        : someIcons\n          ? [\"full\", \"compact\", \"words\"]\n          : [\"full\", \"compact\"];\n    const pick = () => {\n      // the thumb still stands where the last fold put it: out of the measure, so only the segments decide\n      const thumbEl = el.querySelector<HTMLElement>(\"[data-slot=segmented-thumb]\");\n      if (thumbEl) thumbEl.style.display = \"none\";\n      let chosen: Fold = asField ? \"select\" : \"scroll\";\n      for (const f of folds) {\n        el.dataset.fold = f;\n        if (el.scrollWidth <= el.clientWidth + 1) {\n          chosen = f;\n          break;\n        }\n      }\n      el.dataset.fold = chosen;\n      if (thumbEl) thumbEl.style.display = \"\";\n      // into or out of the icon fold the segments are mounted again (only there do they carry a tooltip): the one\n      // holding focus is noted, to be focused again where it now stands\n      const held = el.contains(document.activeElement)\n        ? (document.activeElement as HTMLElement).dataset.value\n        : undefined;\n      if (held !== undefined && (chosen === \"icons\") !== withTips.current) refocus.current = held;\n      setFold((f) => (f === chosen ? f : chosen));\n    };\n    // a scrolling row fades only the edges that have more past them, so the row says which way it goes on\n    const edges = () => {\n      const start = Math.abs(el.scrollLeft) > 1;\n      const end = Math.abs(el.scrollLeft) + el.clientWidth < el.scrollWidth - 1;\n      const v = el.dataset.fold === \"scroll\" ? [start && \"start\", end && \"end\"].filter(Boolean).join(\" \") : \"\";\n      if ((el.dataset.edge ?? \"\") !== v) el.dataset.edge = v;\n    };\n    const pickAndEdge = () => {\n      pick();\n      edges();\n    };\n    pickAndEdge();\n    el.addEventListener(\"scroll\", edges, { passive: true });\n    if (typeof ResizeObserver === \"undefined\") return () => el.removeEventListener(\"scroll\", edges);\n    // the room is the parent's; a pass waits for the next frame so its own change of width never loops back\n    let frame = 0;\n    const observer = new ResizeObserver(() => {\n      cancelAnimationFrame(frame);\n      frame = requestAnimationFrame(pickAndEdge);\n    });\n    if (el.parentElement) observer.observe(el.parentElement);\n    return () => {\n      observer.disconnect();\n      cancelAnimationFrame(frame);\n      el.removeEventListener(\"scroll\", edges);\n    };\n  }, [root, optionSet, iconOnly, allIcons, someIcons, size, fill, asField]);\n  // folded into a scrolling row, the choice is kept in view\n  useLayoutEffect(() => {\n    const el = root.current;\n    if (fold !== \"scroll\" || !el) return;\n    const on = el.querySelector<HTMLElement>(\"[role=radio][aria-checked=true]\");\n    if (!on) return;\n    // by how far its centre is from the row's centre, which reads the same in a right-to-left row\n    const a = on.getBoundingClientRect();\n    const b = el.getBoundingClientRect();\n    el.scrollBy({ left: a.left + a.width / 2 - (b.left + b.width / 2), behavior: \"instant\" as ScrollBehavior });\n  }, [fold, value, root]);\n  const enabled = disabled ? [] : options.filter((o) => !o.disabled);\n  const onKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\";\n    const dir = { ArrowRight: rtl ? -1 : 1, ArrowDown: 1, ArrowLeft: rtl ? 1 : -1, ArrowUp: -1 }[e.key];\n    let next: Segment | undefined;\n    if (dir) {\n      const i = enabled.findIndex((o) => o.value === value);\n      next = enabled[(i + dir + enabled.length) % enabled.length];\n    } else if (e.key === \"Home\") next = enabled[0];\n    else if (e.key === \"End\") next = enabled.at(-1);\n    if (!next) return;\n    e.preventDefault();\n    choose(next.value);\n    segmentFor(root.current, next.value)?.focus();\n  };\n  const chosenAny = options.some((o) => o.value === value);\n  const control = (aria?: ChoiceFieldAria) => (\n    <div\n      ref={rootRef}\n      dir={dir}\n      style={asField ? undefined : style}\n      role=\"radiogroup\"\n      aria-label={aria ? undefined : label}\n      aria-required={required || undefined}\n      aria-disabled={disabled || undefined}\n      {...aria}\n      data-disabled={disabled ? \"\" : undefined}\n      data-field={asField ? \"\" : undefined}\n      data-slot=\"segmented-control\"\n      data-size={size}\n      data-fold={fold}\n      // folded into a select, the segments stay laid out (unseen, out of reach) only so the room can be measured\n      aria-hidden={fold === \"select\" || undefined}\n      inert={fold === \"select\" || undefined}\n      onKeyDown={onKey}\n      className={cn(\n        styles.root,\n        size === \"sm\" && styles.sm,\n        fill && styles.fill,\n\n        // inside a field's grid the control keeps its own width unless it fills\n        asField && !fill && \"self-start justify-self-start\",\n        classNames?.root,\n        !asField && className,\n      )}\n    >\n      {name && <input type=\"hidden\" name={name} value={value ?? \"\"} />}\n      {thumb && (\n        <span ref={thumbRef} aria-hidden data-slot=\"segmented-thumb\" className={cn(styles.thumb, classNames?.thumb)} />\n      )}\n      {options.map((o) => {\n        const on = o.value === value;\n        const segment = (\n          <button\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={on}\n            data-value={o.value}\n            disabled={disabled || o.disabled}\n            // with nothing chosen yet (a required form choice) the first enabled segment takes Tab\n            tabIndex={on || (!chosenAny && o.value === enabled[0]?.value) ? 0 : -1}\n            data-slot=\"segmented-item\"\n            // until the thumb is first placed (a page drawn on a server), the chosen segment wears its paint itself\n            className={cn(styles.segment, on && !thumb && \"m-control\", classNames?.segment)}\n            onClick={() => choose(o.value)}\n            onPointerEnter={() => onOptionIntent?.(o.value)}\n            onFocus={() => onOptionIntent?.(o.value)}\n          >\n            {o.icon}\n            {/* folded to icons the words stay: the segment's name for assistive tech and its tooltip */}\n            <span data-slot=\"segmented-label\" className={styles.label}>\n              {o.label}\n            </span>\n            {(on ? (o.activeNote ?? o.note) : o.note) && (\n              <span\n                data-slot=\"segmented-note\"\n                className={cn(\n                  styles.accessory,\n                  \"inline-grid h-4.5 place-items-center rounded-pill bg-green-tint px-1.5 text-2xs leading-none font-medium text-green\",\n                  classNames?.note,\n                )}\n              >\n                <TextMorph>{(on ? (o.activeNote ?? o.note) : o.note) as string}</TextMorph>\n              </span>\n            )}\n            {o.accessory != null && (\n              <span data-slot=\"segmented-accessory\" className={styles.accessory}>\n                {o.accessory}\n              </span>\n            )}\n          </button>\n        );\n        // folded to icons, a segment names itself in the system tooltip; in every other fold it carries none, since a\n        // closed tooltip on each segment re-rendered with the control (on the landing: 689 renders more at load)\n        return fold === \"icons\" ? (\n          <Tooltip key={o.value} content={o.label}>\n            {segment}\n          </Tooltip>\n        ) : (\n          <Fragment key={o.value}>{segment}</Fragment>\n        );\n      })}\n    </div>\n  );\n  if (!asField) return control();\n  // the select comes first, so in a split form it takes the control's place beside the label; the measured segments\n  // keep their position among the children (and their state) in every fold\n  const narrowest = (aria: ChoiceFieldAria) => (\n    <>\n      {fold === \"select\" && (\n        <Select\n          label={label}\n          showLabel={false}\n          placeholder={i18n(\"chooseOne\")}\n          required={required}\n          size={size === \"sm\" ? \"sm\" : \"md\"}\n          value={value}\n          disabled={disabled}\n          onValueChange={choose}\n          options={options.map((o) => ({ value: o.value, label: o.label, icon: o.icon, disabled: o.disabled }))}\n          className=\"animate-enter-fade\"\n        />\n      )}\n      {control(aria)}\n    </>\n  );\n  return (\n    <ChoiceField\n      slot=\"segmented-control-field\"\n      label={label}\n      hideLabel={hideLabel}\n      required={required}\n      requiredOnControl\n      description={description}\n      error={error}\n      className={cn(classNames?.field, className)}\n      style={style}\n      classNames={classNames}\n    >\n      {narrowest}\n    </ChoiceField>\n  );\n}\n\nexport default SegmentedControl;\n"},{"path":"beamline/components/select/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/select/defaults.ts","content":"export interface SelectDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/select/messages.ts","type":"registry:file","target":"src/ui/beamline/components/select/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  placeholder: \"Select\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/select/select.module.css","type":"registry:file","target":"src/ui/beamline/components/select/select.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* The trigger is a field frame (paint, hover, focus edge, invalid, disabled at --pui-opacity-disabled all come from\n     _shared/field); it adds only what a button-like frame needs. */\n  .trigger {\n    width: 100%;\n    justify-content: space-between;\n    text-align: start;\n    cursor: pointer;\n  }\n  .trigger:focus-visible {\n    outline: none;\n  }\n  .trigger:disabled {\n    cursor: not-allowed;\n  }\n  .trigger[data-placeholder] {\n    color: var(--pui-ink-3);\n  }\n  /* the value line: label, then the detail on the same line or wrapped out of sight */\n  .value {\n    display: flex;\n    min-width: 0;\n    flex: 1;\n    flex-wrap: wrap;\n    align-items: baseline;\n    column-gap: var(--pui-space-2);\n    height: 1lh;\n    overflow: hidden;\n  }\n  .valueDetail {\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    white-space: nowrap;\n  }\n  .chevron {\n    flex-shrink: 0;\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n    color: var(--pui-ink-3);\n  }\n\n  /* list rows: a detail at the end, the check after it at the inline end */\n  .detail {\n    flex-shrink: 0;\n    margin-inline-start: auto;\n    padding-inline-start: var(--pui-space-3);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n  }\n  .check {\n    position: absolute;\n    inset-inline-end: var(--pui-space-2);\n    display: inline-flex;\n  }\n  .check > svg {\n    color: var(--pui-accent);\n  }\n}\n"},{"path":"beamline/components/select/select.tsx","type":"registry:file","target":"src/ui/beamline/components/select/select.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { Fragment, useRef, type CSSProperties, type ReactNode, type Ref } from \"react\";\nimport { FollowPlate } from \"../_shared/glide\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport * as RadixSelect from \"@radix-ui/react-select\";\nimport { Field, useFieldIds, type FieldSize, type FieldClassNames } from \"../_shared/field\";\nimport { usePortalContainer, useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SelectDefaults } from \"./defaults\";\nexport type { SelectDefaults } from \"./defaults\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport field from \"../_shared/field.module.css\";\nimport overlay from \"../_shared/overlay.module.css\";\nimport styles from \"./select.module.css\";\n\nexport interface SelectOption {\n  /** Value represented by this item. Required. */\n  value: string;\n  /** Text displayed for this item. Required. */\n  label: ReactNode;\n  /** Icon element displayed with this item. Omitted by default. */\n  icon?: ReactNode;\n  /** A short detail at the end of the row (an offset, a plan); the field shows it beside the chosen label while it has\n   *  room for it whole. */\n  description?: ReactNode;\n  /** Whether this option is unavailable for selection. Omitted by default. */\n  disabled?: boolean;\n}\nexport interface SelectGroup {\n  /** Text displayed for this item. Required. */\n  label: string;\n  /** Choices available for this question or field. Required. */\n  options: SelectOption[];\n}\n\nexport interface SelectProps {\n  /** Visible label or accessible name for the control. Required. */\n  label: ReactNode;\n  /** Shows the label visually while keeping its accessible name; defaults to true. Takes precedence over hideLabel. */\n  showLabel?: boolean;\n  /** Hides the label; defaults to false. @deprecated Use showLabel={false} instead. */\n  hideLabel?: boolean;\n  /** Supporting text beneath the label or heading. Defaults to no supporting text. */\n  description?: ReactNode;\n  /** Error message shown in place of the usual supporting text. Defaults to no error message. */\n  error?: ReactNode;\n  /** Text shown before a value is entered or selected. Defaults to \"Select\". */\n  placeholder?: string;\n  /** Options, or groups of options with a heading. */\n  options: (SelectOption | SelectGroup)[];\n  /** Controlled value of the chosen option; omit to use defaultValue. */\n  value?: string;\n  /** Initial value of the chosen option when uncontrolled. Defaults to no selected option. */\n  defaultValue?: string;\n  /** Reports value of the chosen option 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  /** Prevents interaction with the control. Defaults to false. */\n  disabled?: boolean;\n  /** Marks the field as required for form submission. Defaults to false. */\n  required?: boolean;\n  /** Field name used in native form submission. Defaults to no form field name. */\n  name?: string;\n  /** DOM id used to connect the control and its label. Defaults to an automatically generated control id. */\n  id?: string;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: FieldClassNames &\n    Partial<Record<\"trigger\" | \"value\" | \"icon\" | \"list\" | \"viewport\" | \"item\" | \"groupLabel\" | \"separator\", string>>;\n  /** Controlled open state; omit to use defaultOpen. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. Defaults to false. */\n  defaultOpen?: boolean;\n  /** Reports open state after a change; no callback by default. */\n  onOpenChange?: (open: boolean) => void;\n  /** Receives the button or trigger; no ref by default. */\n  ref?: Ref<HTMLButtonElement>;\n}\n\nconst isGroup = (o: SelectOption | SelectGroup): o is SelectGroup => \"options\" in o;\n\n/** One choice from a short known list, in the same frame as every text field. Type to jump; the list opens under the\n *  field at its width. */\nexport function Select({\n  label,\n  hideLabel: hideLabelProp,\n  showLabel,\n  description,\n  error,\n  placeholder: placeholderProp,\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n  size: sizeProp,\n  disabled,\n  required,\n  name,\n  id,\n  className,\n  classNames,\n  style,\n  open,\n  defaultOpen,\n  onOpenChange,\n  ref,\n}: SelectProps) {\n  const i18n = useMessages(\"select\", englishMessages);\n  const placeholder = placeholderProp === undefined ? i18n(\"placeholder\") : placeholderProp;\n\n  useDeprecatedProp(\"Select\", \"hideLabel\", \"showLabel={false}\", hideLabelProp !== undefined);\n  const hideLabel = showLabel === undefined ? hideLabelProp : !showLabel;\n  const defaults = useComponentDefaults<SelectDefaults>(\"select\", [\"size\"]);\n  const size = sizeProp ?? defaults.size ?? \"md\";\n  const { controlId, descriptionId, errorId, aria } = useFieldIds(id, { description, error });\n  const container = usePortalContainer();\n  const viewport = useRef<HTMLDivElement>(null);\n  // the choice, kept here too (Radix keeps its own), so the field can show the chosen option's detail\n  const [current, setCurrent] = useControllable(value, defaultValue, onValueChange);\n  const chosen = options.flatMap((o) => (isGroup(o) ? o.options : [o])).find((o) => o.value === current);\n  const item = (o: SelectOption) => (\n    <RadixSelect.Item\n      key={o.value}\n      value={o.value}\n      disabled={o.disabled}\n      data-menu-item=\"\"\n      data-glide=\"\"\n      data-slot=\"select-item\"\n      className={cn(overlay.item, \"pe-8\", classNames?.item)}\n    >\n      {o.icon}\n      <span className=\"min-w-0 truncate\">\n        <RadixSelect.ItemText>{o.label}</RadixSelect.ItemText>\n      </span>\n      {o.description && <span className={styles.detail}>{o.description}</span>}\n      <RadixSelect.ItemIndicator className={styles.check}>\n        <Check />\n      </RadixSelect.ItemIndicator>\n    </RadixSelect.Item>\n  );\n  return (\n    <Field\n      label={label}\n      hideLabel={hideLabel}\n      description={description}\n      error={error}\n      required={required}\n      requiredOnControl\n      controlId={controlId}\n      descriptionId={descriptionId}\n      errorId={errorId}\n      className={className}\n      classNames={classNames}\n      style={style}\n    >\n      <RadixSelect.Root\n        open={open}\n        defaultOpen={defaultOpen}\n        onOpenChange={onOpenChange}\n        value={value}\n        defaultValue={defaultValue}\n        onValueChange={(v) => {\n          setCurrent(v);\n        }}\n        disabled={disabled}\n        required={required}\n        name={name}\n      >\n        <RadixSelect.Trigger\n          ref={ref}\n          data-slot=\"select\"\n          {...aria}\n          className={cn(field.frame, field[size], styles.trigger, classNames?.trigger)}\n          data-invalid={error ? \"\" : undefined}\n        >\n          {/* the label, then the chosen option's detail (UTC+3, Pro plan): the detail wraps out of sight (one line,\n              clipped) when the field is too narrow to show it whole, so a narrow field shows the label alone */}\n          <span data-slot=\"select-value\" className={cn(styles.value, classNames?.value)}>\n            <span className=\"max-w-full truncate\">\n              <RadixSelect.Value placeholder={placeholder} />\n            </span>\n            {chosen?.description && (\n              <span data-slot=\"select-detail\" className={styles.valueDetail}>\n                {chosen.description}\n              </span>\n            )}\n          </span>\n          <RadixSelect.Icon data-slot=\"select-icon\" className={classNames?.icon}>\n            <ChevronsUpDown className={styles.chevron} />\n          </RadixSelect.Icon>\n        </RadixSelect.Trigger>\n        {container !== null && (\n          <RadixSelect.Portal container={container}>\n            <RadixSelect.Content\n              data-slot=\"select-list\"\n              position=\"popper\"\n              sideOffset={6}\n              collisionPadding={8}\n              className={cn(\n                overlay.surface,\n                overlay.menu,\n                classNames?.list,\n                \"max-h-[min(360px,var(--radix-select-content-available-height))] w-[var(--radix-select-trigger-width)]\",\n              )}\n            >\n              <RadixSelect.Viewport\n                ref={viewport}\n                data-slot=\"select-viewport\"\n                className={cn(\"relative\", classNames?.viewport)}\n              >\n                <FollowPlate list={viewport} rows=\"[data-slot=select-item]\" />\n                {options.map((o, i) =>\n                  isGroup(o) ? (\n                    <Fragment key={o.label}>\n                      {i > 0 && (\n                        <RadixSelect.Separator\n                          data-slot=\"select-separator\"\n                          className={cn(overlay.separator, classNames?.separator)}\n                        />\n                      )}\n                      <RadixSelect.Group>\n                        <RadixSelect.Label\n                          data-slot=\"select-group-label\"\n                          className={cn(overlay.heading, classNames?.groupLabel)}\n                        >\n                          {o.label}\n                        </RadixSelect.Label>\n                        {o.options.map(item)}\n                      </RadixSelect.Group>\n                    </Fragment>\n                  ) : (\n                    item(o)\n                  ),\n                )}\n              </RadixSelect.Viewport>\n            </RadixSelect.Content>\n          </RadixSelect.Portal>\n        )}\n      </RadixSelect.Root>\n    </Field>\n  );\n}\n\nexport default Select;\n"},{"path":"beamline/components/skeleton/messages.ts","type":"registry:file","target":"src/ui/beamline/components/skeleton/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 content\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/skeleton/motion-features.ts","type":"registry:file","target":"src/ui/beamline/components/skeleton/motion-features.ts","content":"// REASON: Motion's DOM animation features (tween and presence) load after the first paint, so a Skeleton costs a\n// small part of Motion on first load; LazyMotion in skeleton.tsx renders until they arrive.\nimport { domAnimation } from \"motion/react\";\n\nexport default domAnimation;\n"},{"path":"beamline/components/skeleton/skeleton.tsx","type":"registry:file","target":"src/ui/beamline/components/skeleton/skeleton.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useRef, type ComponentProps, type CSSProperties, type ReactNode, type Ref } from \"react\";\nimport { AnimatePresence, LazyMotion, m } from \"motion/react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useHeightMorph } from \"../../hooks/use-height-morph\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { cn } from \"../../lib/utils\";\n\n// Motion's animation features arrive in a deferred chunk; the swap is instant until they land.\nconst features = () => import(\"./motion-features\").then((module) => module.default);\n\nexport interface SkeletonClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the placeholder part; no override by default. */\n  placeholder?: string;\n  /** Classes for the avatar part; no override by default. */\n  avatar?: string;\n  /** Classes for the lines part; no override by default. */\n  lines?: string;\n  /** Classes for the line part; no override by default. */\n  line?: string;\n  /** Classes for the content part; no override by default. */\n  content?: string;\n}\n\nexport interface SkeletonProps {\n  /** Announced to assistive tech while loading. */\n  label?: string;\n  /** Text lines in the placeholder; the last is shorter. */\n  lines?: number;\n  /** A round avatar before the lines. */\n  avatar?: boolean;\n  /** With children: the placeholder shows while true, the children after. */\n  loading?: boolean;\n  /** The real content. */\n  children?: ReactNode;\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<HTMLDivElement>;\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?: SkeletonClassNames;\n}\n\n// The sheen (utilities.css .sheen) slides a gradient layer with transform, so the compositor moves it without repainting\n// the placeholder on every frame; it pauses with the block (play state) and stops for reduced motion (data-still). A\n// scope's own motion choice (data-motion) and the OS preference also still it in CSS, so a block or group needs no\n// reader of the look's motion values: a part that only shows a placeholder stays small.\nconst SHEEN = \"sheen\";\n\n/** One placeholder shape: give it the width, height and radius of what will be there. */\nexport function SkeletonBlock({\n  className,\n  style,\n  ref,\n}: {\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<HTMLSpanElement>;\n}) {\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const reduced = useReducedMotionSafe();\n  return (\n    <span\n      ref={attach}\n      aria-hidden\n      data-slot=\"skeleton-block\"\n      data-still={reduced ? \"\" : undefined}\n      className={cn(\"block h-3 rounded-chip\", SHEEN, className)}\n      style={{ \"--pui-sheen-play\": active ? \"running\" : \"paused\", ...style } as CSSProperties}\n    />\n  );\n}\n\n/**\n * Many placeholder shapes under one visibility and motion check: a heatmap's cells, a chart's bars. Each shape is a\n * plain element with the `sheen` class anywhere inside the group, which pauses them off screen and stills them under\n * reduced motion together, so a 500-cell placeholder is one component instead of 500, and a list's loading rows can\n * keep the real rows' structure (and so their height).\n */\nexport function SkeletonGroup({ className, style, ref, ...props }: ComponentProps<\"div\">) {\n  const host = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const reduced = useReducedMotionSafe();\n  return (\n    <div\n      ref={attach}\n      data-slot=\"skeleton-group\"\n      className={className}\n      style={{ \"--pui-sheen-play\": active ? \"running\" : \"paused\", ...style } as CSSProperties}\n      {...props}\n      // the shapes' styles key on these, so a caller's own data-slot (a chart's loading area) keeps its name\n      data-skeleton-group=\"\"\n      data-still={reduced ? \"\" : undefined}\n    />\n  );\n}\n\n/**\n * One line of text that has not arrived, inside a SkeletonGroup: an inline box exactly one line of the surrounding text\n * style tall (a zero-width character holds the line), with the shape centred in it. Put the real text's classes on it\n * (`text-sm`, `font-figure text-2xl leading-none`) and a width, and the loaded line takes its place without a shift.\n */\nexport function SkeletonLine({\n  className,\n  style,\n}: {\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}) {\n  return (\n    <span\n      aria-hidden\n      data-slot=\"skeleton-line\"\n      className={cn(\"relative inline-block w-24 align-top\", className)}\n      style={style}\n    >\n      {\"\\u200b\"}\n      <span className=\"sheen absolute inset-x-0 top-1/2 block h-[0.7em] -translate-y-1/2 rounded-chip\" />\n    </span>\n  );\n}\n\n/** Holds the shape of content while it loads, then crossfades into it; if the content is taller or shorter than its\n *  placeholder, the height eases from one to the other, so what sits below glides instead of jumping. */\nexport function Skeleton({\n  label: labelProp,\n  lines = 3,\n  avatar = false,\n  loading = true,\n  children,\n  className,\n  classNames,\n  style,\n  ref,\n}: SkeletonProps) {\n  const i18n = useMessages(\"skeleton\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n\n  const host = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(host, ref);\n  const motionTokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = motionTokens.reduced || managedReduced;\n  const showPlaceholder = children === undefined || loading;\n  // the swap keeps its path: the box eases between the placeholder's height and the content's\n  useHeightMorph(host, showPlaceholder, { clip: true, duration: \"standard\", reduced });\n  // A crossfade: reduced motion keeps it restrained, none removes it (APPEARANCE A06).\n  const fade = {\n    duration: motionTokens.mode === \"none\" ? 0 : reduced ? motionTokens.fade.in : motionTokens.duration.standard,\n    ease: motionTokens.ease.default,\n  };\n  return (\n    <div\n      ref={attach}\n      data-slot=\"skeleton\"\n      data-state={showPlaceholder ? \"loading\" : \"loaded\"}\n      aria-busy={showPlaceholder || undefined}\n      className={cn(\"relative\", classNames?.root, className)}\n      style={style}\n    >\n      <LazyMotion features={features}>\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {showPlaceholder ? (\n            <m.div\n              key=\"placeholder\"\n              role=\"status\"\n              aria-label={label}\n              data-slot=\"skeleton-placeholder\"\n              className={cn(\"flex items-start gap-3\", classNames?.placeholder)}\n              exit={{ opacity: 0 }}\n              transition={fade}\n            >\n              {avatar && <SkeletonBlock className={cn(\"size-9 shrink-0 rounded-full\", classNames?.avatar)} />}\n              <div\n                data-slot=\"skeleton-lines\"\n                className={cn(\"flex min-w-0 flex-1 flex-col gap-2.5 py-1\", classNames?.lines)}\n              >\n                {Array.from({ length: Math.max(1, lines) }, (_, i) => (\n                  <SkeletonBlock\n                    key={i}\n                    className={classNames?.line}\n                    style={{ width: i === lines - 1 && lines > 1 ? \"62%\" : i === 0 ? \"88%\" : \"100%\" }}\n                  />\n                ))}\n              </div>\n            </m.div>\n          ) : (\n            <m.div\n              data-slot=\"skeleton-content\"\n              className={classNames?.content}\n              key=\"content\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              transition={fade}\n            >\n              {children}\n            </m.div>\n          )}\n        </AnimatePresence>\n      </LazyMotion>\n    </div>\n  );\n}\n\nexport default Skeleton;\n"},{"path":"beamline/components/spinner/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/spinner/defaults.ts","content":"/** Pure allowed presentation defaults for Spinner; no component or runtime imports. */\nexport interface SpinnerDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/spinner/messages.ts","type":"registry:file","target":"src/ui/beamline/components/spinner/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  label: \"Loading\",\n  done: \"Done\",\n  error: \"Failed\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/spinner/spinner.module.css","type":"registry:file","target":"src/ui/beamline/components/spinner/spinner.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* With a delay the ring waits unseen and then fades in, so quick work never flashes it; the result (a check or a\n     cross) always shows at once. */\n  .root[data-delay] {\n    animation: appear var(--pui-fade-in) var(--pui-ease-default) var(--_delay) both;\n  }\n  @keyframes appear {\n    from {\n      opacity: 0;\n    }\n  }\n  /* The turn runs on the compositor (a CSS animation on the svg box) and costs no JS per frame. When the work ends the\n     arc closes into a full ring over the standard duration, so stopping the turn is invisible, and the check or cross\n     draws itself a beat later. All timing comes from the motion tokens; the loop period follows --pui-motion-scale. */\n  .svg[data-state=\"loading\"] {\n    animation: turn calc(0.8s * var(--pui-motion-scale, 1) * var(--pui-motion-look, 1)) linear infinite;\n  }\n  .track {\n    stroke-opacity: 0.18;\n  }\n  .arc {\n    stroke-dasharray: 0.3 1;\n    transition: stroke-dasharray var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .svg:not([data-state=\"loading\"]) .arc {\n    stroke-dasharray: 1 1;\n  }\n  .mark {\n    stroke-dasharray: 1 1;\n    animation: draw var(--pui-duration-standard) var(--pui-ease-enter) var(--pui-duration-fast) both;\n  }\n  @keyframes turn {\n    to {\n      transform: rotate(1turn);\n    }\n  }\n  @keyframes draw {\n    from {\n      stroke-dashoffset: 1;\n    }\n    to {\n      stroke-dashoffset: 0;\n    }\n  }\n  .svg[data-paused] {\n    animation-play-state: paused;\n  }\n  .svg[data-reduced],\n  .svg[data-reduced] .mark {\n    animation: none;\n  }\n  /* Reduction retains the loading/result shape without a decorative loop. */\n  @media (prefers-reduced-motion: reduce) {\n    .svg[data-state=\"loading\"],\n    .mark {\n      animation: none;\n    }\n  }\n}\n"},{"path":"beamline/components/spinner/spinner.tsx","type":"registry:file","target":"src/ui/beamline/components/spinner/spinner.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SpinnerDefaults } from \"./defaults\";\nexport type { SpinnerDefaults } from \"./defaults\";\n\nimport { useRef, type CSSProperties, type Ref } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./spinner.module.css\";\n\nexport interface SpinnerClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the svg part; no override by default. */\n  svg?: string;\n  /** Classes for the track part; no override by default. */\n  track?: string;\n  /** Classes for the arc part; no override by default. */\n  arc?: string;\n  /** Classes for the mark part; no override by default. */\n  mark?: string;\n}\n\nexport interface SpinnerProps {\n  /** `success` draws a check out of the ring, `error` a cross; both stop spinning. */\n  state?: \"loading\" | \"success\" | \"error\";\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Accessible name while loading. */\n  label?: string;\n  /** ms to wait before it shows: work that ends sooner never flashes a spinner (about 300 is a good start). Default 0. */\n  delay?: number;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLSpanElement>;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SpinnerClassNames;\n}\n\n/** Icon box from the foundation icon steps (§14.2): sm 14, md 16, lg 20. */\nconst PX = { sm: \"var(--pui-icon-sm)\", md: \"var(--pui-icon-md)\", lg: \"var(--pui-icon-lg)\" } as const;\n\n/**\n * A ring that spins while work runs and closes into a drawn check (or cross) when it ends — the result is a shape as\n * well as a colour. With reduced motion the loading ring holds still.\n */\nexport function Spinner({\n  state = \"loading\",\n  size: sizeProp,\n  label: labelProp,\n  delay = 0,\n  className,\n  classNames,\n  style,\n  ref,\n}: SpinnerProps) {\n  const i18n = useMessages(\"spinner\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n\n  const scopeDefaults = useComponentDefaults<SpinnerDefaults>(\"spinner\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = tokens.reduced || managedReduced;\n\n  const box = PX[size];\n  const done = state !== \"loading\";\n  const tone = state === \"success\" ? \"text-green\" : state === \"error\" ? \"text-red\" : \"text-ink-2\";\n  return (\n    <span\n      ref={attach}\n      data-slot=\"spinner\"\n      data-state={state}\n      data-delay={delay > 0 && state === \"loading\" ? \"\" : undefined}\n      role={done ? \"img\" : \"status\"}\n      aria-label={state === \"success\" ? i18n(\"done\") : state === \"error\" ? i18n(\"error\") : label}\n      className={cn(styles.root, \"relative inline-grid shrink-0 place-items-center\", tone, classNames?.root, className)}\n      style={{ width: box, height: box, \"--_delay\": `${delay}ms`, ...style } as CSSProperties}\n    >\n      <svg\n        viewBox=\"0 0 24 24\"\n        width=\"100%\"\n        height=\"100%\"\n        fill=\"none\"\n        aria-hidden=\"true\"\n        data-state={state}\n        data-slot=\"spinner-svg\"\n        data-reduced={reduced ? \"\" : undefined}\n        data-paused={active ? undefined : \"\"}\n        className={cn(styles.svg, classNames?.svg)}\n      >\n        <circle\n          data-slot=\"spinner-track\"\n          className={cn(styles.track, classNames?.track)}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"9.5\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n        />\n        <circle\n          data-slot=\"spinner-arc\"\n          className={cn(styles.arc, classNames?.arc)}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"9.5\"\n          pathLength={1}\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n          strokeLinecap=\"round\"\n          transform=\"rotate(-90 12 12)\"\n        />\n        {state === \"success\" && (\n          <path\n            data-slot=\"spinner-mark\"\n            className={cn(styles.mark, classNames?.mark)}\n            d=\"M7.5 12.5l3 3 6-6.5\"\n            pathLength={1}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        )}\n        {state === \"error\" && (\n          <path\n            data-slot=\"spinner-mark\"\n            className={cn(styles.mark, classNames?.mark)}\n            d=\"M8.5 8.5l7 7M15.5 8.5l-7 7\"\n            pathLength={1}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.5\"\n            strokeLinecap=\"round\"\n          />\n        )}\n      </svg>\n    </span>\n  );\n}\n\nexport default Spinner;\n"},{"path":"beamline/components/switch/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/switch/defaults.ts","content":"/** Pure allowed presentation defaults for Switch; no component or runtime imports. */\nexport interface SwitchDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\n"},{"path":"beamline/components/switch/messages.ts","type":"registry:file","target":"src/ui/beamline/components/switch/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  saving: \"Saving\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/switch/switch.module.css","type":"registry:file","target":"src/ui/beamline/components/switch/switch.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Switch: a track and a thumb. The track is the toggle role of the look in force (a well that fills when on), the\n     thumb its knob; the thumb slides on the system spring, stretches while pressed and reaches as it travels. */\n  .track {\n    --m-color: var(--pui-control-on);\n    --pui-switch-w: 32px;\n    --_switch-direction: 1;\n    --pui-switch-h: 18px;\n    --pui-switch-pad: 2px;\n    position: relative;\n    flex-shrink: 0;\n    display: inline-flex;\n    align-items: center;\n    width: var(--pui-switch-w);\n    height: var(--pui-switch-h);\n    padding: var(--pui-switch-pad);\n    border-radius: var(--pui-radius-pill);\n    cursor: pointer;\n    transition: var(--m-transition);\n  }\n  .track:dir(rtl) {\n    --_switch-direction: -1;\n  }\n  .track:disabled {\n    opacity: var(--pui-opacity-disabled);\n    cursor: not-allowed;\n  }\n  .track[data-busy] {\n    cursor: progress;\n  }\n  /* invalid: a red ring just outside the track, drawn over whatever edge the look gives it (with the words below) */\n  .track[aria-invalid=\"true\"]::before {\n    content: \"\";\n    position: absolute;\n    inset: calc(var(--pui-space-0-5) * -1);\n    border-radius: inherit;\n    box-shadow: inset 0 0 0 1px var(--pui-red);\n    pointer-events: none;\n  }\n\n  /* labelled: the switch beside its label and the message line; the whole row lights the track */\n  .field {\n    display: flex;\n    align-items: flex-start;\n    gap: var(--pui-space-3);\n  }\n  .text {\n    --pui-field-gap: var(--pui-space-0-5);\n    display: flex;\n    flex-direction: column;\n    min-width: 0;\n    gap: var(--pui-field-gap);\n  }\n  .field:hover:not([data-disabled]) .track:not([data-state=\"checked\"]) {\n    --pui-m-c: var(--m-toggle-off-hover, var(--pui-control-track-hover));\n  }\n  .sm {\n    --pui-switch-w: 26px;\n    --pui-switch-h: 14px;\n  }\n  .thumb {\n    --pui-thumb: calc(var(--pui-switch-h) - var(--pui-switch-pad) * 2);\n    display: grid;\n    place-items: center;\n    width: var(--pui-thumb);\n    height: var(--pui-thumb);\n    border-radius: var(--pui-radius-pill);\n    transition:\n      transform var(--pui-duration-spring) var(--pui-ease-spring),\n      width var(--pui-duration-fast) var(--pui-ease-default);\n  }\n  .thumb[data-state=\"checked\"] {\n    background: var(--m-knob-sheen), var(--pui-control-thumb-on);\n    transform: translateX(\n      calc((var(--pui-switch-w) - var(--pui-thumb) - var(--pui-switch-pad) * 2) * var(--_switch-direction))\n    );\n  }\n  /* toggled: the knob reaches toward where it is going (its leading side stretches by half) and gathers there, a bead\n     of the look's material, never overshooting; rigid looks keep their knob (--m-morph-spread 0) */\n  .thumb[data-travel=\"on\"] {\n    animation: reach-on var(--pui-duration-spring) var(--pui-ease-default);\n  }\n  .thumb[data-travel=\"off\"] {\n    animation: reach-off var(--pui-duration-spring) var(--pui-ease-default);\n  }\n  @keyframes reach-on {\n    40% {\n      width: calc(var(--pui-thumb) * (1 + var(--m-morph-spread, 0.35) * 1.4));\n    }\n  }\n  @keyframes reach-off {\n    40% {\n      width: calc(var(--pui-thumb) * (1 + var(--m-morph-spread, 0.35) * 1.4));\n      translate: calc(var(--pui-thumb) * var(--m-morph-spread, 0.35) * -1.4 * var(--_switch-direction)) 0;\n    }\n  }\n  .track:active:not(:disabled) .thumb {\n    width: calc(var(--pui-thumb) + 4px);\n  }\n  .track:active:not(:disabled) .thumb[data-state=\"checked\"] {\n    transform: translateX(\n      calc((var(--pui-switch-w) - var(--pui-thumb) - 4px - var(--pui-switch-pad) * 2) * var(--_switch-direction))\n    );\n  }\n  /* saving: the shared spinner turns inside the knob (it pauses off screen and stops under reduced motion) */\n  .busy {\n    scale: 0.72;\n    color: var(--pui-ink-2);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.track {\n  composes: m-toggle from global;\n}\n.thumb {\n  composes: m-knob from global;\n}\n"},{"path":"beamline/components/switch/switch.tsx","type":"registry:file","target":"src/ui/beamline/components/switch/switch.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SwitchDefaults } from \"./defaults\";\nexport type { SwitchDefaults } from \"./defaults\";\n\nimport { useId, useState, type ComponentProps, type ReactNode } from \"react\";\nimport * as RadixSwitch from \"@radix-ui/react-switch\";\nimport { FieldMessage } from \"../_shared/field\";\nimport { Spinner } from \"../spinner/spinner\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./switch.module.css\";\n\nexport type SwitchClassNames = Partial<\n  Record<\"root\" | \"control\" | \"thumb\" | \"text\" | \"label\" | \"description\" | \"error\", string>\n>;\n\nexport interface SwitchProps extends ComponentProps<typeof RadixSwitch.Root> {\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SwitchClassNames;\n  /** Visible label; clicking it toggles. Without it, pass aria-label. */\n  label?: ReactNode;\n  /** Supporting text beneath the label or heading. Defaults to no supporting text. */\n  description?: ReactNode;\n  /** The problem and its fix (\"Could not save. Try again.\"); replaces the description and marks the switch invalid. */\n  error?: ReactNode;\n  /** The setting is being saved: a spinner turns in the knob, the switch is aria-busy and holds its place. */\n  loading?: boolean;\n  /** Where the label sits: before the switch (a settings row) or after it. */\n  labelPosition?: \"start\" | \"end\";\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: \"sm\" | \"md\";\n}\n\n/** A setting that applies at once. checked / defaultChecked / onCheckedChange. */\nexport function Switch({\n  label,\n  description,\n  error,\n  loading,\n  labelPosition = \"end\",\n  size: sizeProp,\n  id,\n  className,\n  style,\n  classNames,\n  disabled,\n  ref,\n  onCheckedChange,\n  onClick,\n  ...props\n}: SwitchProps) {\n  const i18n = useMessages(\"switch\", englishMessages);\n\n  const scopeDefaults = useComponentDefaults<SwitchDefaults>(\"switch\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  // the knob travels as a bead: it reaches toward the side it is going to and gathers there (switch.module.css)\n  const [travel, setTravel] = useState<\"on\" | \"off\" | null>(null);\n  const auto = useId();\n  const switchId = id ?? `switch-${auto}`;\n  const descId = description && !error ? `${switchId}-description` : undefined;\n  const errorId = error ? `${switchId}-error` : undefined;\n  const control = (\n    <RadixSwitch.Root\n      ref={ref}\n      style={!label ? style : undefined}\n      id={switchId}\n      data-slot=\"switch\"\n      disabled={disabled}\n      aria-describedby={[descId, errorId].filter(Boolean).join(\" \") || undefined}\n      aria-invalid={error ? true : undefined}\n      aria-errormessage={errorId}\n      aria-busy={loading || undefined}\n      data-busy={loading ? \"\" : undefined}\n      onCheckedChange={(next) => {\n        setTravel(next ? \"on\" : \"off\");\n        onCheckedChange?.(next);\n      }}\n      onClick={(e) => {\n        onClick?.(e);\n        // while a change is saving the switch holds (and keeps its focus): a second press waits for the first\n        if (loading) e.preventDefault();\n      }}\n      className={cn(styles.track, styles[size], classNames?.control, !label && classNames?.root, !label && className)}\n      {...props}\n    >\n      <RadixSwitch.Thumb\n        data-slot=\"switch-thumb\"\n        data-travel={travel ?? undefined}\n        onAnimationEnd={(e) => {\n          if (e.target === e.currentTarget) setTravel(null);\n        }}\n        className={cn(styles.thumb, classNames?.thumb)}\n      >\n        {loading && <Spinner size=\"sm\" label={i18n(\"saving\")} className={styles.busy} />}\n      </RadixSwitch.Thumb>\n    </RadixSwitch.Root>\n  );\n  if (!label) return control;\n  const text = (\n    <div data-slot=\"switch-text\" className={cn(styles.text, classNames?.text)}>\n      <label\n        htmlFor={switchId}\n        data-slot=\"switch-label\"\n        className={cn(\n          \"cursor-pointer text-sm leading-[var(--pui-leading-body)] font-medium text-ink select-none group-data-[disabled]:cursor-not-allowed group-data-[disabled]:opacity-[var(--pui-opacity-disabled)]\",\n          classNames?.label,\n        )}\n      >\n        {label}\n      </label>\n      {/* the same line every field has: a problem grows in where the help was and folds away when it is fixed */}\n      <FieldMessage\n        error={error}\n        description={description}\n        errorId={errorId}\n        descriptionId={descId}\n        slot=\"switch\"\n        classNames={{ error: classNames?.error, description: classNames?.description }}\n      />\n    </div>\n  );\n  return (\n    <div\n      data-slot=\"switch-field\"\n      data-disabled={disabled ? \"\" : undefined}\n      data-invalid={error ? \"\" : undefined}\n      style={style}\n      className={cn(\"group\", styles.field, labelPosition === \"start\" && \"justify-between\", classNames?.root, className)}\n    >\n      {labelPosition === \"start\" ? (\n        <>\n          {text}\n          {control}\n        </>\n      ) : (\n        <>\n          {control}\n          {text}\n        </>\n      )}\n    </div>\n  );\n}\n\nexport default Switch;\n"},{"path":"beamline/components/text-morph/text-morph.tsx","type":"registry:file","target":"src/ui/beamline/components/text-morph/text-morph.tsx","content":"\"use client\";\n\nimport { useMemo, useRef, type CSSProperties, type Ref } from \"react\";\nimport { AnimatePresence, LayoutGroup, motion } from \"motion/react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { useWidthMorph } from \"../../hooks/use-width-morph\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface TextMorphClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the letter part; no override by default. */\n  letter?: string;\n}\n\nexport interface TextMorphProps {\n  /** A short label; when it changes, letters it shares with the old one slide into place and the rest fade. */\n  children: string;\n  /** HTML element used for the text or container. Defaults to \"span\". */\n  as?: \"span\" | \"div\" | \"p\" | \"strong\" | \"h1\" | \"h2\" | \"h3\";\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** DOM id on the animated text root; omitted by default. */\n  id?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: TextMorphClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLElement>;\n}\n\n/** A short label that becomes its next state letter by letter: shared letters glide, new ones arrive, old ones leave,\n *  and the label's width eases with them, so whatever sits after it (a button, a counter) glides instead of jumping.\n *  Reduced motion swaps the text. Assistive tech reads the label once, not the letters. */\nexport function TextMorph({ children, as = \"span\", className, classNames, style, ref, id }: TextMorphProps) {\n  const host = useRef<HTMLElement>(null);\n  const attach = useComposedRef(host, ref);\n  const motionTokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = motionTokens.reduced || managedReduced;\n  const Tag = motion[as];\n  useWidthMorph(host, reduced ? null : children);\n  // key each letter by its character and how many times it has appeared so far, so repeats map in order\n  const letters = useMemo(() => {\n    const seen: Record<string, number> = {};\n    return [...children].map((ch) => {\n      seen[ch] = (seen[ch] ?? 0) + 1;\n      return { ch, key: `${ch}-${seen[ch]}` };\n    });\n  }, [children]);\n  // a script written right to left is joined letter to letter and can't be split into glyphs: it swaps whole, like\n  // reduced motion; everything else keeps its own left-to-right letter order even on a right-to-left page\n  const rtlScript = /[\\u0590-\\u08FF\\uFB1D-\\uFDFF\\uFE70-\\uFEFF]/.test(children);\n  if (reduced || rtlScript)\n    return (\n      <Tag ref={attach} id={id} data-slot=\"text-morph\" className={cn(classNames?.root, className)} style={style}>\n        {children}\n      </Tag>\n    );\n  return (\n    <Tag\n      ref={attach}\n      id={id}\n      data-slot=\"text-morph\"\n      aria-label={children}\n      dir=\"ltr\"\n      className={cn(\"inline-flex overflow-x-clip whitespace-pre\", classNames?.root, className)}\n      style={style}\n    >\n      <LayoutGroup>\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {letters.map(({ ch, key }) => (\n            <motion.span\n              key={key}\n              aria-hidden\n              layout=\"position\"\n              // the letters move only when the text changes: a re-render with the same words (a scroll, a resize, a\n              // parent's update) leaves them still\n              layoutDependency={children}\n              data-slot=\"text-morph-letter\"\n              className={cn(\"inline-block\", classNames?.letter)}\n              initial={{ opacity: 0, filter: `blur(${motionTokens.blur.subtle}px)`, y: 3 }}\n              animate={{ opacity: 1, filter: \"blur(0px)\", y: 0 }}\n              exit={{ opacity: 0, filter: `blur(${motionTokens.blur.subtle}px)`, y: -3 }}\n              transition={motionTokens.spring.morph}\n            >\n              {ch === \" \" ? \" \" : ch}\n            </motion.span>\n          ))}\n        </AnimatePresence>\n      </LayoutGroup>\n    </Tag>\n  );\n}\n\nexport default TextMorph;\n"},{"path":"beamline/components/time-picker/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/time-picker/defaults.ts","content":"/** Pure allowed presentation defaults for TimePicker; no component or runtime imports. */\nexport interface TimePickerDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/time-picker/messages.ts","type":"registry:file","target":"src/ui/beamline/components/time-picker/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  chooseFromAList: \"Choose from a list\",\n  hours: \"Hours\",\n  empty: \"Empty\",\n  minutes: \"Minutes\",\n  aMOrPM: \"AM or PM\",\n  aM: \"AM\",\n  pM: \"PM\",\n  times: \"Times\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/time-picker/time-picker.tsx","type":"registry:file","target":"src/ui/beamline/components/time-picker/time-picker.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { TimePickerDefaults } from \"./defaults\";\nimport { cssEase } from \"../../lib/motion-tokens\";\nexport type { TimePickerDefaults } from \"./defaults\";\n\nimport { useEffect, useRef, useState, type KeyboardEvent, type Ref, type CSSProperties, type ReactNode } from \"react\";\nimport { Check, Clock } from \"lucide-react\";\nimport * as RadixPopover from \"@radix-ui/react-popover\";\nimport { Field, FieldFrame, useFieldIds, type FieldClassNames, type FieldSize } from \"../_shared/field\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { IconAction } from \"../_shared/icon-action\";\nimport { FollowPlate, GlidePlate } from \"../_shared/glide\";\nimport { rollText } from \"../_shared/roll\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport overlay from \"../_shared/overlay.module.css\";\n\nexport interface TimePickerProps {\n  /** Visible label or accessible name for the control. Required. */\n  label: ReactNode;\n  /** Shows the label visually while keeping its accessible name; defaults to true. Takes precedence over hideLabel. */\n  showLabel?: boolean;\n  /** Hides the label; defaults to false. @deprecated Use showLabel={false} instead. */\n  hideLabel?: boolean;\n  /** \"HH:mm\", 24-hour, whatever the display format. */\n  value?: string | null;\n  /** Initial 24-hour hh:mm time when uncontrolled. Defaults to null. */\n  defaultValue?: string | null;\n  /** Legacy value-change callback; omitted by default. Prefer onValueChange.\n   *  @deprecated Use onValueChange; this value callback remains supported and is omitted by default. */\n  onChange?: (value: string) => void;\n  /** The new time (\"HH:MM\"): the name every Beamline field uses. */\n  onValueChange?: (value: string) => void;\n  /** Supporting text beneath the label or heading. Defaults to no supporting text. */\n  description?: ReactNode;\n  /** Error message shown in place of the usual supporting text. Defaults to no error message. */\n  error?: ReactNode;\n  /** Minutes move by this much, and the list steps by it (at least 15). Default 5. */\n  minuteStep?: 1 | 5 | 10 | 15 | 30;\n  /** Hour notation used in the field and time list. Defaults to \"24h\". */\n  format?: \"12h\" | \"24h\";\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: FieldSize;\n  /** Prevents interaction with the control. Defaults to false. */\n  disabled?: boolean;\n  /** Marks the field as required for form submission. Defaults to false. */\n  required?: boolean;\n  /** DOM id used to connect the control and its label. Defaults to an automatically generated control id. */\n  id?: string;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for documented field parts; root style does not replace native behavior. */\n  classNames?: FieldClassNames & Partial<Record<\"group\" | \"segment\" | \"trigger\" | \"content\" | \"list\" | \"item\", string>>;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the hours spinbutton; no ref by default. */\n  ref?: Ref<HTMLSpanElement>;\n}\n\nconst pad = (n: number) => String(n).padStart(2, \"0\");\nconst parse = (v?: string | null) => {\n  const m = v?.match(/^(\\d{1,2}):(\\d{2})$/);\n  return m ? { h: Math.min(23, +m[1]), m: Math.min(59, +m[2]) } : null;\n};\n\n/**\n * A time of day as hour and minute segments, typed or stepped: ↑ ↓ change the focused part, digits fill it and move on,\n * ← → move between parts, A or P sets the period. The clock opens a list of times.\n */\nexport function TimePicker({\n  label,\n  hideLabel: hideLabelProp,\n  showLabel,\n  value: valueProp,\n  defaultValue = null,\n  onChange,\n  onValueChange,\n  description,\n  error,\n  minuteStep = 5,\n  format = \"24h\",\n  size: sizeProp,\n  disabled,\n  required,\n  id,\n  className,\n  classNames,\n  style,\n  ref,\n}: TimePickerProps) {\n  const i18n = useMessages(\"time-picker\", englishMessages);\n\n  useDeprecatedProp(\"TimePicker\", \"hideLabel\", \"showLabel={false}\", hideLabelProp !== undefined);\n  const hideLabel = showLabel === undefined ? hideLabelProp : !showLabel;\n  useDeprecatedProp(\"TimePicker\", \"onChange\", \"onValueChange\", onChange !== undefined);\n  const scopeDefaults = useComponentDefaults<TimePickerDefaults>(\"time-picker\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  // a commit that lands on the same time is not a change, so the hook holds the value and the report is guarded\n  const [value, setValue] = useControllable(valueProp, defaultValue);\n  const t = parse(value);\n  const [draft, setDraft] = useState<{ h: number | null; m: number | null }>({ h: t?.h ?? null, m: t?.m ?? null });\n  useEffect(() => {\n    const p = parse(value);\n    setDraft({ h: p?.h ?? null, m: p?.m ?? null });\n  }, [value]);\n  const [open, setOpen] = useState(false);\n  const typed = useRef(\"\");\n  const parts = useRef<(HTMLSpanElement | null)[]>([]);\n  const firstPart = useRef<HTMLSpanElement | null>(null);\n  const attach = useComposedRef(firstPart, ref);\n  const list = useRef<HTMLUListElement>(null);\n  const group = useRef<HTMLDivElement>(null);\n  // which part holds focus: one accent plate travels between the parts instead of each lighting on its own\n  const [focused, setFocused] = useState<number | null>(null);\n  const motion = useMotionTokens(group);\n  const { controlId, descriptionId, errorId, aria } = useFieldIds(id, { description, error });\n  const container = usePortalContainer();\n  const h12 = format === \"12h\";\n\n  const commit = (h: number | null, m: number | null) => {\n    setDraft({ h, m });\n    if (h === null || m === null) return;\n    const v = `${pad(h)}:${pad(m)}`;\n    setValue(v);\n    if (v !== value) {\n      onChange?.(v);\n      onValueChange?.(v);\n    }\n  };\n  const go = (i: number) => (i === 0 ? firstPart.current : parts.current[i])?.focus();\n  // a stepped part rolls to its next value (up for more, down for less); a typed digit never animates\n  const roll = (el: HTMLSpanElement, from: string, to: string, d: number) => {\n    if (motion.reduced || from === to) return;\n    const ease = motion.ease.standard;\n    // rollText draws still copies over any positioned text box; it is typed for inputs (see the report's proposal)\n    rollText(el, from, to, d > 0 ? 1 : -1, {\n      duration: motion.duration.standard * 1000,\n      easing: cssEase(ease, \"ease-out\"),\n    });\n  };\n  const segKey = (seg: \"h\" | \"m\" | \"p\", i: number) => (e: KeyboardEvent<HTMLSpanElement>) => {\n    const { h, m } = draft;\n    const el = e.currentTarget;\n    const text = (hh: number | null, mm: number | null) =>\n      seg === \"h\"\n        ? hh === null\n          ? \"--\"\n          : pad(h12 ? hh % 12 || 12 : hh)\n        : seg === \"m\"\n          ? mm === null\n            ? \"--\"\n            : pad(mm)\n          : hh === null\n            ? \"--\"\n            : hh < 12\n              ? \"AM\"\n              : \"PM\";\n    const set = (nh: number, nm: number, d: number) => {\n      roll(el, text(h, m), text(nh, nm), d);\n      commit(nh, nm);\n    };\n    const bump = (d: number) => {\n      if (seg === \"h\") set(((h ?? 0) + d + 24) % 24, m ?? 0, d);\n      else if (seg === \"m\") set(h ?? 9, ((((m ?? 0) + d * minuteStep) % 60) + 60) % 60, d);\n      else set(((h ?? 9) + 12) % 24, m ?? 0, d);\n    };\n    const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\";\n    if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      bump(1);\n    } else if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      bump(-1);\n    } else if (e.key === (rtl ? \"ArrowLeft\" : \"ArrowRight\")) {\n      e.preventDefault();\n      go(i + 1);\n    } else if (e.key === (rtl ? \"ArrowRight\" : \"ArrowLeft\")) {\n      e.preventDefault();\n      go(i - 1);\n    } else if (e.key === \"Home\" || e.key === \"End\") {\n      // a spin button's ends: the first and last value the part takes\n      e.preventDefault();\n      const end = e.key === \"End\";\n      if (seg === \"h\")\n        set(end ? (h12 ? ((h ?? 0) >= 12 ? 23 : 11) : 23) : h12 ? ((h ?? 0) >= 12 ? 12 : 0) : 0, m ?? 0, end ? 1 : -1);\n      else if (seg === \"m\") set(h ?? 9, end ? 60 - minuteStep : 0, end ? 1 : -1);\n    } else if (e.key === \"Backspace\" || e.key === \"Delete\") {\n      e.preventDefault();\n      typed.current = \"\";\n      setDraft(seg === \"h\" ? { h: null, m } : { h, m: null });\n    } else if (seg === \"p\" && /^[ap]$/i.test(e.key)) {\n      e.preventDefault();\n      const pm = e.key.toLowerCase() === \"p\";\n      const base = (h ?? 9) % 12;\n      set(pm ? base + 12 : base, m ?? 0, pm ? 1 : -1);\n    } else if (/^\\d$/.test(e.key) && seg !== \"p\") {\n      e.preventDefault();\n      typed.current = (typed.current + e.key).slice(-2);\n      let n = Number(typed.current);\n      if (seg === \"h\") {\n        if (h12) {\n          n = n > 12 ? Number(e.key) : n;\n          const pm = (h ?? 0) >= 12;\n          n = n === 12 ? (pm ? 12 : 0) : pm ? (n % 12) + 12 : n % 12;\n        } else if (n > 23) {\n          typed.current = e.key;\n          n = Number(e.key);\n        }\n        commit(n, m ?? 0);\n        if (typed.current.length === 2 || Number(typed.current) > (h12 ? 1 : 2)) {\n          typed.current = \"\";\n          go(i + 1);\n        }\n      } else {\n        if (n > 59) {\n          typed.current = e.key;\n          n = Number(e.key);\n        }\n        commit(h ?? 9, n);\n        if (typed.current.length === 2 || Number(typed.current) > 5) {\n          typed.current = \"\";\n          if (h12) go(i + 1);\n        }\n      }\n    }\n  };\n  const shownHour = draft.h === null ? null : h12 ? draft.h % 12 || 12 : draft.h;\n  // the focused part's ink turns over to the accent's foreground once the plate has arrived under it (never white on\n  // the field while the plate is on its way); it turns back at once\n  const seg =\n    \"relative z-1 rounded-[calc(var(--pui-radius-chip)-2px)] px-0.5 font-numeric tabular-nums outline-none transition-colors duration-0 focus:text-[var(--pui-accent-foreground)] focus:delay-[var(--pui-duration-fast)]\";\n  const segFocus = (i: number) => ({\n    \"data-part\": i,\n    onFocus: () => setFocused(i),\n    onBlur: () => {\n      typed.current = \"\";\n      setFocused((f) => (f === i ? null : f));\n    },\n  });\n  const step = Math.max(15, minuteStep);\n  const times = Array.from({ length: (24 * 60) / step }, (_, i) => {\n    const mins = i * step;\n    return { h: Math.floor(mins / 60), m: mins % 60 };\n  });\n  const label12 = (h: number, m: number) =>\n    h12 ? `${h % 12 || 12}:${pad(m)} ${h < 12 ? \"AM\" : \"PM\"}` : `${pad(h)}:${pad(m)}`;\n\n  return (\n    <Field\n      label={label}\n      hideLabel={hideLabel}\n      description={description}\n      error={error}\n      required={required}\n      controlId={controlId}\n      descriptionId={descriptionId}\n      errorId={errorId}\n      className={className}\n      classNames={classNames}\n      style={style}\n    >\n      <RadixPopover.Root open={open} onOpenChange={setOpen}>\n        <RadixPopover.Anchor asChild>\n          <div>\n            <FieldFrame\n              classNames={classNames}\n              size={size}\n              invalid={!!error}\n              disabled={disabled}\n              trailing={\n                <RadixPopover.Trigger asChild>\n                  <IconAction\n                    data-slot=\"time-picker-trigger\"\n                    aria-label={i18n(\"chooseFromAList\")}\n                    disabled={disabled}\n                    className={classNames?.trigger}\n                  >\n                    <Clock className=\"size-4\" />\n                  </IconAction>\n                </RadixPopover.Trigger>\n              }\n            >\n              <div\n                ref={group}\n                // a time reads 10:30 AM in every direction (numbers stay left to right), so its parts and their arrow\n                // keys do too\n                dir=\"ltr\"\n                role=\"group\"\n                {...aria}\n                aria-label={typeof label === \"string\" ? label : undefined}\n                data-slot=\"time-picker\"\n                className={cn(\"relative flex flex-1 items-center gap-0.5 text-ink rtl:justify-end\", classNames?.group)}\n                onPointerDown={(e) => {\n                  if (e.target === e.currentTarget) {\n                    e.preventDefault();\n                    go(0);\n                  }\n                }}\n              >\n                <GlidePlate\n                  list={group}\n                  selector={focused === null ? null : `[data-part=\"${focused}\"]`}\n                  axis=\"xy\"\n                  className=\"rounded-[calc(var(--pui-radius-chip)-2px)] bg-accent\"\n                />\n                <span\n                  ref={attach}\n                  data-slot=\"time-picker-segment\"\n                  role=\"spinbutton\"\n                  tabIndex={disabled ? -1 : 0}\n                  aria-label={i18n(\"hours\")}\n                  aria-valuenow={shownHour ?? undefined}\n                  aria-valuetext={shownHour === null ? i18n(\"empty\") : String(shownHour)}\n                  aria-valuemin={h12 ? 1 : 0}\n                  aria-valuemax={h12 ? 12 : 23}\n                  onKeyDown={segKey(\"h\", 0)}\n                  {...segFocus(0)}\n                  className={cn(seg, classNames?.segment)}\n                >\n                  {shownHour === null ? <span className=\"text-ink-3\">--</span> : pad(shownHour)}\n                </span>\n                <span aria-hidden className=\"text-ink-3\">\n                  :\n                </span>\n                <span\n                  ref={(el) => {\n                    parts.current[1] = el;\n                  }}\n                  data-slot=\"time-picker-segment\"\n                  role=\"spinbutton\"\n                  tabIndex={disabled ? -1 : 0}\n                  aria-label={i18n(\"minutes\")}\n                  aria-valuenow={draft.m ?? undefined}\n                  aria-valuetext={draft.m === null ? i18n(\"empty\") : pad(draft.m)}\n                  aria-valuemin={0}\n                  aria-valuemax={59}\n                  onKeyDown={segKey(\"m\", 1)}\n                  {...segFocus(1)}\n                  className={cn(seg, classNames?.segment)}\n                >\n                  {draft.m === null ? <span className=\"text-ink-3\">--</span> : pad(draft.m)}\n                </span>\n                {h12 && (\n                  <span\n                    ref={(el) => {\n                      parts.current[2] = el;\n                    }}\n                    data-slot=\"time-picker-segment\"\n                    role=\"spinbutton\"\n                    tabIndex={disabled ? -1 : 0}\n                    aria-label={i18n(\"aMOrPM\")}\n                    aria-valuetext={draft.h === null ? i18n(\"empty\") : draft.h < 12 ? i18n(\"aM\") : i18n(\"pM\")}\n                    onKeyDown={segKey(\"p\", 2)}\n                    {...segFocus(2)}\n                    className={cn(seg, \"ms-1\", classNames?.segment)}\n                  >\n                    {draft.h === null ? <span className=\"text-ink-3\">--</span> : draft.h < 12 ? i18n(\"aM\") : i18n(\"pM\")}\n                  </span>\n                )}\n              </div>\n            </FieldFrame>\n          </div>\n        </RadixPopover.Anchor>\n        {container !== null && (\n          <RadixPopover.Portal container={container}>\n            <RadixPopover.Content\n              data-slot=\"time-picker-content\"\n              align=\"end\"\n              sideOffset={6}\n              collisionPadding={8}\n              className={cn(overlay.surface, overlay.menu, \"w-40 p-0\", classNames?.content)}\n              onOpenAutoFocus={(e) => {\n                e.preventDefault();\n                const el =\n                  list.current?.querySelector<HTMLElement>(\"[aria-selected=true]\") ??\n                  list.current?.querySelector<HTMLElement>(\"[data-near]\");\n                el?.scrollIntoView({ block: \"center\" });\n                el?.focus();\n              }}\n            >\n              <ul\n                ref={list}\n                data-slot=\"time-picker-list\"\n                role=\"listbox\"\n                aria-label={i18n(\"times\")}\n                className={cn(\"relative max-h-64 overflow-y-auto p-1\", classNames?.list)}\n                onKeyDown={(e) => {\n                  const items = [...(list.current?.querySelectorAll<HTMLElement>(\"[role=option]\") ?? [])];\n                  const i = items.indexOf(document.activeElement as HTMLElement);\n                  if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n                    e.preventDefault();\n                    items[Math.max(0, Math.min(items.length - 1, i + (e.key === \"ArrowDown\" ? 1 : -1)))]?.focus();\n                  }\n                }}\n              >\n                <FollowPlate list={list} rows=\"[role=option]\" />\n                {times.map(({ h, m }) => {\n                  const on = draft.h === h && draft.m === m;\n                  const near = !t && h === 9 && m === 0;\n                  return (\n                    <li\n                      key={`${h}:${m}`}\n                      data-slot=\"time-picker-item\"\n                      role=\"option\"\n                      aria-selected={on}\n                      tabIndex={-1}\n                      data-near={near ? \"\" : undefined}\n                      onClick={() => {\n                        commit(h, m);\n                        setOpen(false);\n                      }}\n                      onKeyDown={(e) => {\n                        if (e.key === \"Enter\" || e.key === \" \") {\n                          e.preventDefault();\n                          commit(h, m);\n                          setOpen(false);\n                        }\n                      }}\n                      className={cn(\n                        overlay.item,\n                        \"cursor-pointer font-numeric text-sm tabular-nums outline-none\",\n                        on && \"text-accent-ink\",\n                        classNames?.item,\n                      )}\n                    >\n                      {label12(h, m)}\n                      {/* the chosen time is checked as well as tinted: colour is never the only sign */}\n                      {on && <Check aria-hidden className={overlay.check} />}\n                    </li>\n                  );\n                })}\n              </ul>\n            </RadixPopover.Content>\n          </RadixPopover.Portal>\n        )}\n      </RadixPopover.Root>\n    </Field>\n  );\n}\n\nexport default TimePicker;\n"},{"path":"beamline/components/toast/messages.ts","type":"registry:file","target":"src/ui/beamline/components/toast/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: \"Notifications\",\n  altT: \"{label} (Alt+T)\",\n  dismiss: \"Dismiss\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/toast/toast.module.css","type":"registry:file","target":"src/ui/beamline/components/toast/toast.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* The toast stack. Collapsed, the newest card sits at the edge and up to two older ones peek behind it as clipped\n   edges; pointing at or focusing the stack fans every card out at its own height. Cards arrive from the edge on\n   --pui-ease-drawer and leave by fading over --pui-motion-out; reduced motion makes both instant. */\n  .stack {\n    z-index: var(--pui-z-toast);\n    width: min(360px, calc(100% - 2 * var(--pui-space-4)));\n    outline: none;\n  }\n  .stack[data-reduced] :is(.list, .toast, .body, .close) {\n    transition: none;\n  }\n  .fixed {\n    position: fixed;\n  }\n  .contained {\n    position: absolute;\n  }\n  .stack[data-position=\"bottom-right\"] {\n    right: var(--pui-space-4);\n    bottom: var(--pui-space-4);\n  }\n  .stack[data-position=\"bottom-left\"] {\n    left: var(--pui-space-4);\n    bottom: var(--pui-space-4);\n  }\n  .stack[data-position=\"bottom-center\"] {\n    left: 50%;\n    bottom: var(--pui-space-4);\n    translate: -50% 0;\n  }\n  .stack[data-position=\"top-right\"] {\n    right: var(--pui-space-4);\n    top: var(--pui-space-4);\n  }\n  .stack[data-position=\"top-left\"] {\n    left: var(--pui-space-4);\n    top: var(--pui-space-4);\n  }\n  .stack[data-position=\"top-center\"] {\n    left: 50%;\n    top: var(--pui-space-4);\n    translate: -50% 0;\n  }\n\n  .list {\n    position: relative;\n    margin: 0;\n    padding: 0;\n    list-style: none;\n    transition: height var(--pui-duration-considered) var(--pui-ease-drawer);\n  }\n\n  .toast {\n    position: absolute;\n    inset-inline: 0;\n    bottom: 0;\n    transform-origin: 50% 0;\n    transition:\n      transform var(--pui-duration-considered) var(--pui-ease-drawer),\n      opacity var(--pui-duration-standard) var(--pui-ease-default),\n      height var(--pui-duration-considered) var(--pui-ease-drawer);\n    touch-action: pan-y;\n    outline: none;\n  }\n  .stack[data-position^=\"top\"] .toast {\n    bottom: auto;\n    top: 0;\n    transform-origin: 50% 100%;\n  }\n  .toast[data-leaving] {\n    opacity: 0;\n    transition-duration: var(--pui-motion-out);\n    pointer-events: none;\n  }\n  .toast[data-hidden] {\n    opacity: 0;\n    pointer-events: none;\n  }\n  .toast[data-dragging] {\n    transition: none;\n  }\n\n  .card {\n    /* the card answers to its own width: narrow, the action moves under the text */\n    container-type: inline-size;\n    position: relative;\n    overflow: hidden;\n    height: 100%;\n    border-radius: var(--pui-radius-card);\n    color: var(--pui-ink);\n  }\n  .toast:focus-visible .card {\n    outline: var(--pui-focus-width) var(--pui-focus-style) var(--pui-focus-ring);\n    outline-offset: 2px;\n  }\n  .body {\n    display: grid;\n    grid-template-columns: auto minmax(0, 1fr) auto;\n    align-items: start;\n    gap: var(--pui-space-2-5);\n    padding: var(--pui-space-3);\n    transition: opacity var(--pui-duration-standard) var(--pui-ease-default);\n  }\n  .toast[data-behind] .body {\n    opacity: 0;\n  }\n  /* the type's mark; an update (loading → success) brings the new one in from the middle */\n  .icon {\n    display: grid;\n    place-items: center;\n    width: var(--pui-size-4-5);\n    height: 1lh;\n    font-size: var(--pui-text-sm);\n    line-height: var(--pui-leading-body);\n    animation: mark-in var(--pui-motion-in) var(--pui-ease-out-strong);\n  }\n  .icon :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n  .action {\n    margin-block: calc(-1 * var(--pui-space-0-5));\n  }\n  /* new words after an update fade in where the old ones stood */\n  .content {\n    min-width: 0;\n    animation: words-in var(--pui-fade-in) var(--pui-ease-default);\n  }\n  @container (width < 18rem) {\n    .body {\n      grid-template-columns: auto minmax(0, 1fr);\n    }\n    .action {\n      grid-column: 2;\n      justify-self: start;\n      margin-block: 0;\n    }\n  }\n  .title {\n    font-size: var(--pui-text-sm);\n    font-weight: var(--pui-weight-medium);\n    line-height: var(--pui-leading-body);\n  }\n  .description {\n    margin-top: var(--pui-space-px);\n    color: var(--pui-ink-2);\n    font-size: var(--pui-text-xs);\n    line-height: 1.45;\n  }\n  /* dismiss: a small round button on the card's leading top corner, a shared reveal: shown while the card is pointed\n     at or focused, always on touch and under data-reveal=\"always\" */\n  .close {\n    position: absolute;\n    top: calc(-1 * var(--pui-space-1-5));\n    inset-inline-start: calc(-1 * var(--pui-space-1-5));\n    z-index: 1;\n    display: grid;\n    place-items: center;\n    width: var(--pui-size-5);\n    height: var(--pui-size-5);\n    border-radius: 50%;\n    color: var(--pui-ink-3);\n  }\n  .close:hover {\n    color: var(--pui-ink);\n  }\n  .toast[data-behind] .close {\n    display: none;\n  }\n}\n@keyframes mark-in {\n  from {\n    opacity: 0;\n    scale: calc(1 - 0.4 * var(--pui-motion-travel, 1));\n  }\n}\n@keyframes words-in {\n  from {\n    opacity: 0;\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.card {\n  composes: m-overlay from global;\n}\n.close {\n  composes: m-control from global;\n}\n"},{"path":"beamline/components/toast/toast.tsx","type":"registry:file","target":"src/ui/beamline/components/toast/toast.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type CSSProperties,\n  type Ref,\n  type ReactNode,\n} from \"react\";\nimport { AlertTriangle, CheckCircle2, Info, X, XCircle } from \"lucide-react\";\nimport { Button } from \"../button/button\";\nimport { Spinner } from \"../spinner/spinner\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useDrag } from \"../../hooks/use-drag\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { useAppearanceRoot } from \"../../runtime/appearance\";\nimport { resolveCssTime } from \"../../lib/css-values\";\nimport { focusAfterRemoval } from \"../../lib/focus\";\nimport { cn } from \"../../lib/utils\";\nimport { localId } from \"../../lib/id\";\nimport styles from \"./toast.module.css\";\n\nexport type ToastType = \"success\" | \"error\" | \"warning\" | \"info\" | \"loading\";\nexport type ToastPosition = \"bottom-right\" | \"bottom-left\" | \"bottom-center\" | \"top-right\" | \"top-left\" | \"top-center\";\n\nexport interface ToastInput {\n  /** Reuse an id to replace a toast instead of adding one. */\n  id?: string;\n  type?: ToastType;\n  title: ReactNode;\n  description?: ReactNode;\n  /** One follow-up, e.g. Undo or Retry. Running it dismisses the toast. */\n  action?: { label: string; onClick: () => void };\n  /** ms on screen; Infinity keeps it until dismissed. Defaults: 4 s, warnings 6 s, errors 8 s, loading until updated. */\n  duration?: number;\n}\ninterface ToastRecord extends ToastInput {\n  id: string;\n  type: ToastType;\n  version: number;\n  leaving?: boolean;\n}\n\nconst DURATION: Record<ToastType, number> = {\n  success: 4000,\n  info: 4000,\n  warning: 6000,\n  error: 8000,\n  loading: Infinity,\n};\nconst EXIT_MS = 200;\nconst PEEK = 10;\nconst GAP = 8;\n\ninterface ToastApi {\n  toasts: ToastRecord[];\n  /** Shows a toast and returns its id. */\n  toast: (input: ToastInput) => string;\n  /** Changes a toast in place (loading → success); its timer restarts. */\n  update: (id: string, patch: Partial<ToastInput>) => void;\n  /** Dismisses one toast, or all when no id is given. */\n  dismiss: (id?: string) => void;\n}\ntype ToastContext = ToastApi & { registerCard: (id: string, node: HTMLLIElement | null) => void };\nconst Ctx = createContext<ToastContext | null>(null);\n\n/** Holds the toasts for everything inside it; put one ToastStack inside to show them. */\nexport function ToastStackProvider({ children, max = 20 }: { children: ReactNode; max?: number }) {\n  const [toasts, setToasts] = useState<ToastRecord[]>([]);\n  const cards = useRef(new Map<string, HTMLLIElement>());\n  const timers = useRef(new Set<ReturnType<typeof setTimeout>>());\n  const scope = useAppearanceRoot();\n  const reduced = useReducedMotionSafe();\n  const registerCard = useCallback((id: string, node: HTMLLIElement | null) => {\n    if (node) cards.current.set(id, node);\n    else cards.current.delete(id);\n  }, []);\n  useEffect(\n    () => () => {\n      for (const timer of timers.current) clearTimeout(timer);\n      timers.current.clear();\n    },\n    [],\n  );\n  const toast = useCallback(\n    (input: ToastInput) => {\n      const id = input.id ?? localId(\"toast\");\n      setToasts((list) =>\n        [...list.filter((t) => t.id !== id), { type: \"info\" as ToastType, ...input, id, version: 0 }].slice(-max),\n      );\n      return id;\n    },\n    [max],\n  );\n  const update = useCallback(\n    (id: string, patch: Partial<ToastInput>) =>\n      setToasts((list) => list.map((t) => (t.id === id ? { ...t, ...patch, version: t.version + 1 } : t))),\n    [],\n  );\n  const dismiss = useCallback(\n    (id?: string) => {\n      const nodes = id\n        ? [cards.current.get(id)].filter((node): node is HTMLLIElement => !!node)\n        : [...cards.current.values()];\n      const delay = nodes.length\n        ? Math.max(\n            ...nodes.map((node) =>\n              node.closest(\"[data-slot=toast-stack]\")?.hasAttribute(\"data-reduced\")\n                ? 0\n                : resolveCssTime(node, \"--pui-motion-out\", EXIT_MS / 1000) * 1000,\n            ),\n          )\n        : reduced\n          ? 0\n          : scope\n            ? resolveCssTime(scope, \"--pui-motion-out\", EXIT_MS / 1000) * 1000\n            : EXIT_MS;\n      for (const node of nodes) {\n        const stack = node.closest<HTMLElement>(\"[data-slot=toast-stack]\");\n        focusAfterRemoval(node, { scope: stack, fallback: stack });\n      }\n      setToasts((list) => list.map((t) => (!id || t.id === id ? { ...t, leaving: true } : t)));\n      const timer = setTimeout(() => {\n        timers.current.delete(timer);\n        setToasts((list) => list.filter((t) => !(t.leaving && (!id || t.id === id))));\n      }, delay);\n      timers.current.add(timer);\n    },\n    [scope, reduced],\n  );\n  const api = useMemo(\n    () => ({ toasts, toast, update, dismiss, registerCard }),\n    [toasts, toast, update, dismiss, registerCard],\n  );\n  return <Ctx.Provider value={api}>{children}</Ctx.Provider>;\n}\n\n/** toast(), update() and dismiss() for the nearest ToastStackProvider. */\nexport function useToastStack(): ToastApi {\n  const api = useContext(Ctx);\n  if (!api) throw new Error(\"useToastStack needs a <ToastStackProvider> above it.\");\n  return api;\n}\n\nexport type ToastStackClassNames = Partial<\n  Record<\n    \"root\" | \"list\" | \"toast\" | \"card\" | \"body\" | \"icon\" | \"content\" | \"title\" | \"description\" | \"action\" | \"close\",\n    string\n  >\n>;\n\nexport interface ToastStackProps {\n  classNames?: ToastStackClassNames;\n  ref?: Ref<HTMLElement>;\n  style?: CSSProperties;\n  /** The region's name for assistive tech. */\n  label?: string;\n  position?: ToastPosition;\n  /** Position inside the nearest positioned parent instead of the viewport (a panel, a demo). */\n  contained?: boolean;\n  /** Cards visible while collapsed: the newest and the ones peeking behind it. */\n  visibleToasts?: number;\n  /** Alt+T moves focus to the newest toast. */\n  hotkey?: boolean;\n  className?: string;\n}\n\n/**\n * Short results of an action (saved, sent, failed with retry, undo), stacked at an edge. Pointing at or focusing the\n * stack fans the cards out and pauses their timers; Escape dismisses the focused one; a card can be swiped away.\n */\nexport function ToastStack({\n  label: labelProp,\n  position = \"bottom-right\",\n  contained = false,\n  visibleToasts = 3,\n  hotkey = true,\n  className,\n  classNames,\n  ref,\n  style,\n}: ToastStackProps) {\n  const i18n = useMessages(\"toast\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n\n  const { toasts, dismiss } = useToastStack();\n  const { registerCard } = useContext(Ctx)!;\n  const reduced = useReducedMotionSafe();\n  const [expanded, setExpanded] = useState(false);\n  const [heights, setHeights] = useState<Record<string, number>>({});\n  const [docHidden, setDocHidden] = useState(false);\n  const region = useRef<HTMLElement>(null);\n  const regionRef = useComposedRef(region, ref);\n  const top = position.startsWith(\"top\");\n\n  useEffect(() => {\n    const on = () => setDocHidden(document.hidden);\n    document.addEventListener(\"visibilitychange\", on);\n    return () => document.removeEventListener(\"visibilitychange\", on);\n  }, []);\n  useEffect(() => {\n    if (!hotkey) return;\n    const on = (e: KeyboardEvent) => {\n      if (e.altKey && e.code === \"KeyT\") {\n        e.preventDefault();\n        region.current?.querySelector<HTMLElement>(\"[data-slot=toast]:not([data-leaving])\")?.focus();\n      }\n    };\n    window.addEventListener(\"keydown\", on);\n    return () => window.removeEventListener(\"keydown\", on);\n  }, [hotkey]);\n  const live = toasts.filter((t) => !t.leaving).reverse(); // newest first\n  useEffect(() => {\n    if (!live.length) setExpanded(false);\n  }, [live.length]);\n  const onHeight = useCallback((id: string, h: number) => setHeights((m) => (m[id] === h ? m : { ...m, [id]: h })), []);\n\n  const frontHeight = heights[live[0]?.id] ?? 0;\n  const offsets: Record<string, number> = {};\n  let sum = 0;\n  for (const t of live) {\n    offsets[t.id] = sum;\n    sum += (heights[t.id] ?? 0) + GAP;\n  }\n  const listHeight = !live.length\n    ? 0\n    : expanded\n      ? sum - GAP\n      : frontHeight + (Math.min(live.length, visibleToasts) - 1) * PEEK;\n\n  return (\n    <section\n      ref={regionRef}\n      style={style}\n      data-reduced={reduced ? \"\" : undefined}\n      data-slot=\"toast-stack\"\n      data-position={position}\n      data-expanded={expanded ? \"\" : undefined}\n      aria-label={hotkey ? i18n(\"altT\", { label: label }) : label}\n      tabIndex={-1}\n      className={cn(styles.stack, contained ? styles.contained : styles.fixed, classNames?.root, className)}\n      onPointerEnter={(e) => {\n        if (e.pointerType === \"mouse\") setExpanded(true);\n      }}\n      onPointerLeave={() => setExpanded(false)}\n      onFocus={() => setExpanded(true)}\n      onBlur={(e) => {\n        if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setExpanded(false);\n      }}\n    >\n      <ol data-slot=\"toast-list\" className={cn(styles.list, classNames?.list)} style={{ height: listHeight }}>\n        {[...toasts].reverse().map((t) => {\n          const index = Math.max(0, live.indexOf(t));\n          const sign = top ? 1 : -1;\n          const y = expanded ? sign * offsets[t.id] : sign * index * PEEK;\n          const scale = expanded ? 1 : 1 - index * 0.05;\n          return (\n            <ToastCard\n              classNames={classNames}\n              registerCard={registerCard}\n              reduced={reduced}\n              key={t.id}\n              toast={t}\n              index={index}\n              top={top}\n              y={y}\n              scale={scale}\n              height={!expanded && index > 0 ? frontHeight : undefined}\n              behind={!expanded && index > 0}\n              hidden={index >= visibleToasts && !expanded}\n              paused={expanded || docHidden}\n              onHeight={onHeight}\n              onDismiss={dismiss}\n            />\n          );\n        })}\n      </ol>\n    </section>\n  );\n}\n\nconst ICON: Record<ToastType, ReactNode> = {\n  success: <CheckCircle2 className=\"text-green\" />,\n  error: <XCircle className=\"text-red\" />,\n  warning: <AlertTriangle className=\"text-orange\" />,\n  info: <Info className=\"text-accent\" />,\n  loading: <Spinner size=\"sm\" />,\n};\n\nfunction ToastCard({\n  classNames,\n  registerCard,\n  reduced,\n  toast: t,\n  index,\n  top,\n  y,\n  scale,\n  height,\n  behind,\n  hidden,\n  paused,\n  onHeight,\n  onDismiss,\n}: {\n  classNames?: ToastStackClassNames;\n  registerCard: ToastContext[\"registerCard\"];\n  reduced: boolean;\n  toast: ToastRecord;\n  index: number;\n  top: boolean;\n  y: number;\n  scale: number;\n  height?: number;\n  behind: boolean;\n  hidden: boolean;\n  paused: boolean;\n  onHeight: (id: string, h: number) => void;\n  onDismiss: (id: string) => void;\n}) {\n  const i18n = useMessages(\"toast\", englishMessages);\n\n  const registered = useCallback((node: HTMLLIElement | null) => registerCard(t.id, node), [t.id, registerCard]);\n  const card = useRef<HTMLLIElement | null>(null);\n  const cardRef = useComposedRef(card, registered);\n  const body = useRef<HTMLDivElement>(null);\n  const [entered, setEntered] = useState(false);\n  const [drag, setDrag] = useState<number | null>(null);\n  // a card swiped away keeps going the way it was thrown while it fades, instead of springing back to its slot\n  const [flung, setFlung] = useState(0);\n  const remaining = useRef<number | null>(null);\n  const version = useRef(t.version);\n\n  useLayoutEffect(() => {\n    const el = body.current;\n    if (!el) return;\n    const measure = () => onHeight(t.id, el.offsetHeight);\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [t.id, onHeight]);\n  useEffect(() => {\n    const raf = requestAnimationFrame(() => setEntered(true));\n    return () => cancelAnimationFrame(raf);\n  }, []);\n\n  // the timer runs only while the stack is collapsed and the page visible; an update restarts it\n  useEffect(() => {\n    if (version.current !== t.version) {\n      version.current = t.version;\n      remaining.current = null;\n    }\n    const total = t.duration ?? DURATION[t.type];\n    if (!Number.isFinite(total) || t.leaving || paused) return;\n    remaining.current ??= total;\n    const began = Date.now();\n    const timer = setTimeout(() => onDismiss(t.id), remaining.current);\n    return () => {\n      clearTimeout(timer);\n      if (remaining.current !== null) remaining.current = Math.max(0, remaining.current - (Date.now() - began));\n    };\n  }, [paused, t.version, t.leaving, t.type, t.duration, t.id, onDismiss]);\n\n  // swiped sideways through the shared drag (BUILDING §7): past 64 px or thrown, it carries on and leaves; Escape mid-\n  // swipe puts it back (and does not dismiss it); a finger that starts vertically scrolls the page\n  useDrag(\n    card,\n    {\n      onPress: (_, e) => ((e.target as HTMLElement).closest(\"button, a\") ? false : undefined),\n      onStart: (p) => setDrag(p.dx),\n      onMove: (p) => setDrag(p.dx),\n      onEnd: (p) => {\n        const thrown = Math.abs(p.vx) > 800 && Math.sign(p.vx) === Math.sign(p.dx) && Math.abs(p.dx) > 16;\n        if (Math.abs(p.dx) > 64 || thrown) {\n          setFlung(Math.sign(p.dx) * ((card.current?.offsetWidth ?? 0) + 32));\n          onDismiss(t.id);\n        }\n        setDrag(null);\n      },\n      onCancel: () => setDrag(null),\n    },\n    { axis: \"x\" },\n  );\n\n  const offscreen = top ? -110 : 110;\n  // leaving on its own (timer, ×, Escape) it sinks a little as it fades; thrown, it carries on sideways\n  const leave = t.leaving && !flung && !reduced ? 0.96 : 1;\n  const transform =\n    !entered && !reduced\n      ? `translateY(${offscreen}%)`\n      : `translate(${drag ?? flung}px, ${y}px) scale(${scale * leave})`;\n  return (\n    <li\n      ref={cardRef}\n      data-slot=\"toast\"\n      data-type={t.type}\n      tabIndex={0}\n      inert={t.leaving || undefined}\n      data-behind={behind ? \"\" : undefined}\n      data-hidden={hidden ? \"\" : undefined}\n      data-leaving={t.leaving ? \"\" : undefined}\n      data-dragging={drag ? \"\" : undefined}\n      className={cn(styles.toast, \"reveal-host\", classNames?.toast)}\n      style={{\n        transform,\n        zIndex: 100 - index,\n        height,\n        opacity: drag ? Math.max(0.2, 1 - Math.abs(drag) / 160) : undefined,\n      }}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\") {\n          e.stopPropagation();\n          onDismiss(t.id);\n        }\n      }}\n    >\n      <div\n        data-slot=\"toast-card\"\n        role={t.type === \"error\" ? \"alert\" : \"status\"}\n        aria-atomic\n        className={cn(styles.card, classNames?.card)}\n      >\n        <div data-slot=\"toast-body\" ref={body} className={cn(styles.body, classNames?.body)}>\n          <span key={t.type} data-slot=\"toast-icon\" className={cn(styles.icon, classNames?.icon)}>\n            {ICON[t.type]}\n          </span>\n          <div key={t.version} data-slot=\"toast-content\" className={cn(styles.content, classNames?.content)}>\n            <div data-slot=\"toast-title\" className={cn(styles.title, classNames?.title)}>\n              {t.title}\n            </div>\n            {t.description && (\n              <div data-slot=\"toast-description\" className={cn(styles.description, classNames?.description)}>\n                {t.description}\n              </div>\n            )}\n          </div>\n          {t.action && (\n            <Button\n              size=\"sm\"\n              variant=\"secondary\"\n              className={cn(styles.action, classNames?.action)}\n              onClick={() => {\n                t.action?.onClick();\n                onDismiss(t.id);\n              }}\n            >\n              {t.action.label}\n            </Button>\n          )}\n        </div>\n      </div>\n      {/* organism-allow: raw-control -- a round badge that overhangs the toast's corner */}\n      <button\n        data-slot=\"toast-close\"\n        type=\"button\"\n        aria-label={i18n(\"dismiss\")}\n        className={cn(styles.close, \"reveal\", classNames?.close)}\n        onClick={() => onDismiss(t.id)}\n      >\n        <X className=\"size-3\" />\n      </button>\n    </li>\n  );\n}\n\nexport default ToastStack;\n"},{"path":"beamline/components/toggle-group/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/toggle-group/defaults.ts","content":"/** Pure allowed presentation defaults for ToggleGroup; no component or runtime imports. */\nexport interface ToggleGroupDefaults {\n  /** Shared variant setting; omit to inherit the surrounding scope or the component default. */\n  variant?: \"outline\" | \"ghost\" | \"default\";\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\" | \"default\";\n}\n"},{"path":"beamline/components/toggle-group/toggle-group.module.css","type":"registry:file","target":"src/ui/beamline/components/toggle-group/toggle-group.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Toggle group: bare toggles (ghost) or a framed row (outline). A toggle that is on sits pressed in: it takes the\n     paint of the look's inset well, so on reads as \"held down\" in every look. Sizes are set on the group. */\n  .root {\n    --pui-toggle-h: 32px;\n    --pui-toggle-pad: 8px;\n    position: relative;\n    isolation: isolate;\n    display: inline-flex;\n    max-width: 100%;\n    flex-wrap: wrap; /* in a room narrower than the row, toggles wrap rather than spill */\n    align-items: center;\n    gap: var(--pui-space-0-5);\n  }\n  .outline {\n    /* the well's inset is size, not spacing: a control keeps its height when the Spacing parameter changes (O1 §14.2) */\n    padding: var(--pui-size-0-5, 0.125rem);\n    border-radius: var(--pui-radius-control);\n    box-shadow: inset 0 0 0 1px var(--pui-line-strong);\n  }\n  .sm {\n    --pui-toggle-h: 28px;\n    --pui-toggle-pad: 6px;\n  }\n  .lg {\n    --pui-toggle-h: 40px;\n    --pui-toggle-pad: 10px;\n  }\n  .item {\n    display: inline-flex;\n    height: var(--pui-toggle-h);\n    min-width: var(--pui-toggle-h);\n    align-items: center;\n    justify-content: center;\n    gap: var(--pui-space-1-5);\n    padding-inline: var(--pui-toggle-pad);\n    border-radius: calc(var(--pui-radius-control) - 2px);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-sm);\n    font-weight: var(--pui-weight-medium);\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      box-shadow var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .item:hover {\n    background: var(--pui-hover);\n    color: var(--pui-ink);\n    transition-duration: 0ms;\n  }\n  .item:active:not(:disabled) {\n    scale: var(--pui-press-scale);\n  }\n  /* \"On\" is read from the ARIA state (aria-pressed for several, aria-checked for one): a tooltip around a toggle\n     names its own open state in data-state, so that attribute is not the toggle's. The pressed well is a layer under\n     the toggle, so neighbours that are both on can merge into one run: the facing corners square off and each well\n     reaches into the gap, its own edge clipped at the seam, so a run of Bold Italic Underline reads as one held-down\n     strip; turning one off splits it again (research/morphing-interfaces-2026-10-04, merge). Corners and reach move\n     on the morph spring; nothing overshoots. */\n  .item {\n    position: relative;\n    isolation: isolate;\n  }\n  .item::before {\n    content: \"\";\n    position: absolute;\n    inset: 0;\n    z-index: -1;\n    border-radius: inherit;\n    background: var(--m-inset-shade), var(--pui-inset);\n    /* the look's pressed rim over the well's own edge (first shadow on top), so the eye finds the pressed toggle in every look */\n    box-shadow: var(--m-pressed-rim, 0 0 transparent), var(--m-inset-edge);\n    opacity: 0;\n    transition:\n      opacity var(--pui-motion-out) var(--pui-ease-default),\n      inset var(--pui-duration-morph) var(--pui-ease-standard),\n      border-radius var(--pui-duration-morph) var(--pui-ease-standard),\n      clip-path var(--pui-duration-morph) var(--pui-ease-standard);\n    clip-path: inset(0);\n  }\n  .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"]) {\n    color: var(--pui-ink);\n  }\n  .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"])::before {\n    opacity: 1;\n    transition-duration: 0ms, var(--pui-duration-morph), var(--pui-duration-morph), var(--pui-duration-morph);\n  }\n  /* joined to the next toggle */\n  .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"]):has(\n      + .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"])\n    )::before {\n    inset-inline-end: calc(-1 * var(--pui-space-0-5));\n    border-start-end-radius: 0;\n    border-end-end-radius: 0;\n    clip-path: inset(0 1px 0 0);\n  }\n  .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"]):has(\n      + .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"])\n    ):dir(rtl)::before {\n    clip-path: inset(0 0 0 1px);\n  }\n  /* joined to the previous toggle */\n  .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"])\n    + .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"])::before {\n    inset-inline-start: calc(-1 * var(--pui-space-0-5));\n    border-start-start-radius: 0;\n    border-end-start-radius: 0;\n    clip-path: inset(0 0 0 1px);\n  }\n  .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"])\n    + .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"]):dir(rtl)::before {\n    clip-path: inset(0 1px 0 0);\n  }\n  /* joined on both sides */\n  .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"])\n    + .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"]):has(\n      + .item:is([aria-pressed=\"true\"], [aria-checked=\"true\"])\n    )::before {\n    clip-path: inset(0 1px);\n  }\n  .item:disabled {\n    opacity: var(--pui-opacity-disabled);\n    pointer-events: none;\n  }\n\n  /* one choice: a single pressed well under the toggles travels to the new choice as matter (ui/lib/contour.ts, placed\n     by the contour engine like the segmented thumb); the toggles' own wells stand aside */\n  .well {\n    position: absolute;\n    z-index: -1;\n    top: 0;\n    left: 0;\n    border-radius: calc(var(--pui-radius-control) - 2px);\n    background: var(--m-inset-shade), var(--pui-inset);\n    box-shadow: var(--m-pressed-rim, 0 0 transparent), var(--m-inset-edge);\n    pointer-events: none;\n  }\n  .root[data-type=\"single\"] .item::before {\n    display: none;\n  }\n  .root[data-type=\"single\"] .item {\n    isolation: auto;\n  }\n  .root[data-type=\"single\"] .item:hover:is([aria-pressed=\"true\"], [aria-checked=\"true\"]) {\n    background: transparent;\n  }\n  .item :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n}\n"},{"path":"beamline/components/toggle-group/toggle-group.tsx","type":"registry:file","target":"src/ui/beamline/components/toggle-group/toggle-group.tsx","content":"\"use client\";\n\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { ToggleGroupDefaults } from \"./defaults\";\nexport type { ToggleGroupDefaults } from \"./defaults\";\n\nimport type { ComponentProps, ReactNode } from \"react\";\nimport * as RadixToggleGroup from \"@radix-ui/react-toggle-group\";\nimport { Tooltip } from \"../tooltip/tooltip\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useContour } from \"../../hooks/use-contour\";\nimport { useIndicator } from \"../../hooks/use-indicator\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./toggle-group.module.css\";\n\ntype Variant = \"ghost\" | \"outline\";\ntype Size = \"sm\" | \"md\" | \"lg\";\n\nexport type ToggleGroupProps = ComponentProps<typeof RadixToggleGroup.Root> & {\n  /** Visual weight; defaults to ghost. @deprecated The value \"default\" aliases \"ghost\"; use \"ghost\" instead. */\n  variant?: Variant | \"default\";\n  /** Control size; defaults to md. @deprecated The value \"default\" aliases \"md\"; use \"md\" instead. */\n  size?: Size | \"default\";\n};\n\n/**\n * Pressable toggles where one (type=\"single\") or several (type=\"multiple\") can be on: alignment, bold / italic,\n * view options. ← → move between them; Space or Enter presses. A toggle that is on sits pressed in: with one choice the\n * pressed well travels to the new one as matter; with several, neighbours that are both on merge into one run.\n */\nexport function ToggleGroup({\n  variant: variantProp,\n  size: sizeProp,\n  className,\n  children,\n  ref,\n  ...props\n}: ToggleGroupProps) {\n  const scopeDefaults = useComponentDefaults<ToggleGroupDefaults>(\"toggle-group\", [\"variant\", \"size\"]);\n  const variant = variantProp ?? scopeDefaults.variant ?? \"ghost\";\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  useDeprecatedProp(\"ToggleGroup\", 'variant=\"default\"', 'variant=\"ghost\"', variant === \"default\");\n  useDeprecatedProp(\"ToggleGroup\", 'size=\"default\"', 'size=\"md\"', size === \"default\");\n  const look = variant === \"default\" ? \"ghost\" : variant;\n  const scale = size === \"default\" ? \"md\" : size;\n  const single = props.type === \"single\";\n  // one choice: a single pressed well follows it (the segmented thumb's contour), so a change travels instead of\n  // fading out here and in there\n  const [root, well] = useIndicator<HTMLDivElement>(\"[data-slot=toggle-group-item][aria-checked=true]\", single);\n  const wellRef = useContour<HTMLSpanElement>(single ? well : null);\n  const attach = useComposedRef(root, ref);\n  return (\n    <RadixToggleGroup.Root\n      ref={attach}\n      data-slot=\"toggle-group\"\n      data-variant={look}\n      data-size={scale}\n      data-type={props.type}\n      className={cn(styles.root, look === \"outline\" && styles.outline, scale !== \"md\" && styles[scale], className)}\n      {...props}\n    >\n      {single && well && <span ref={wellRef} aria-hidden data-slot=\"toggle-group-well\" className={styles.well} />}\n      {children}\n    </RadixToggleGroup.Root>\n  );\n}\n\nexport type ToggleGroupItemProps = ComponentProps<typeof RadixToggleGroup.Item> & {\n  /** What the toggle does, in a few words, shown on hover and focus; it also names an icon-only toggle. */\n  tooltip?: ReactNode;\n  /** A shortcut shown in the tooltip, e.g. \"⌘B\". */\n  shortcut?: string;\n};\n\nexport function ToggleGroupItem({ className, tooltip, shortcut, ...props }: ToggleGroupItemProps) {\n  const item = (\n    <RadixToggleGroup.Item\n      data-slot=\"toggle-group-item\"\n      aria-label={props[\"aria-label\"] ?? (typeof tooltip === \"string\" ? tooltip : undefined)}\n      className={cn(styles.item, className)}\n      {...props}\n    />\n  );\n  if (tooltip == null) return item;\n  return (\n    <Tooltip content={tooltip} shortcut={shortcut}>\n      {item}\n    </Tooltip>\n  );\n}\n\nexport default ToggleGroup;\n"},{"path":"beamline/components/tooltip/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/tooltip/defaults.ts","content":"/** Pure allowed presentation defaults for Tooltip; no component or runtime imports. */\nexport interface TooltipDefaults {\n  /** Shared align setting; omit to inherit the surrounding scope or the component default. */\n  align?: \"start\" | \"center\" | \"end\";\n}\n"},{"path":"beamline/components/tooltip/tooltip.module.css","type":"registry:file","target":"src/ui/beamline/components/tooltip/tooltip.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A tooltip arrives from its control: it starts a step toward the control on the side it opens on and moves away\n     into place as it fades in (--pui-motion-in), so on any side it reads as coming out of what it explains. Inside a\n     group, the next one opens at once where the last one stood and travels to its own control (--pui-duration-fast),\n     while the last one goes in the arrival time: one tooltip moving along the toolbar, not two blinking. It leaves\n     over the departure fade. Reduced motion keeps the fades and drops the travel (--pui-motion-travel is 0). */\n  .tip {\n    --_dx: 0px;\n    --_dy: 0px;\n    --_tx: 0px;\n    --_ty: 0px;\n    z-index: var(--pui-z-tooltip);\n    transform-origin: var(--radix-tooltip-content-transform-origin);\n  }\n  .tip[data-side=\"top\"] {\n    --_dy: var(--pui-space-1);\n  }\n  .tip[data-side=\"bottom\"] {\n    --_dy: calc(-1 * var(--pui-space-1));\n  }\n  .tip[data-side=\"left\"] {\n    --_dx: var(--pui-space-1);\n  }\n  .tip[data-side=\"right\"] {\n    --_dx: calc(-1 * var(--pui-space-1));\n  }\n  .tip[data-state=\"delayed-open\"] {\n    animation: tip-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  .tip[data-state=\"instant-open\"] {\n    animation: tip-fade var(--pui-fade-in) var(--pui-ease-default);\n  }\n  .tip[data-state=\"instant-open\"][data-travel] {\n    animation: tip-travel var(--pui-duration-fast) var(--pui-ease-standard);\n  }\n  .tip[data-state=\"closed\"] {\n    animation: tip-out var(--pui-fade-out) var(--pui-ease-default) forwards;\n  }\n  .tip[data-grouped][data-state=\"closed\"] {\n    animation-duration: var(--pui-fade-in);\n  }\n}\n@keyframes tip-in {\n  from {\n    opacity: 0;\n    translate: calc(var(--_dx) * var(--pui-motion-travel, 1)) calc(var(--_dy) * var(--pui-motion-travel, 1));\n  }\n}\n@keyframes tip-fade {\n  from {\n    opacity: 0;\n  }\n}\n@keyframes tip-travel {\n  from {\n    translate: calc(var(--_tx) * var(--pui-motion-travel, 1)) calc(var(--_ty) * var(--pui-motion-travel, 1));\n  }\n}\n@keyframes tip-out {\n  to {\n    opacity: 0;\n  }\n}\n"},{"path":"beamline/components/tooltip/tooltip.tsx","type":"registry:file","target":"src/ui/beamline/components/tooltip/tooltip.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { TooltipDefaults } from \"./defaults\";\nexport type { TooltipDefaults } from \"./defaults\";\n\nimport {\n  createContext,\n  useContext,\n  useRef,\n  useState,\n  type CSSProperties,\n  type Ref,\n  type ReactElement,\n  type ReactNode,\n  type RefObject,\n} from \"react\";\nimport * as RadixTooltip from \"@radix-ui/react-tooltip\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./tooltip.module.css\";\n\n/** How long after one tooltip of a group closes the next opens at once (and travels from where the last one was). */\nconst SKIP = 400;\n/** The last control of a group whose tooltip showed: its centre and when its tooltip opened or closed. */\ntype Last = { el: Element; x: number; y: number; at: number };\nconst Grouped = createContext<RefObject<Last | null> | null>(null);\n\n/** Tooltips inside a group open instantly one after another (a toolbar): only the first waits, and each next one\n *  travels from where the last one stood. */\nexport function TooltipGroup({\n  children,\n  delay = 500,\n}: {\n  /** Content rendered inside the component. Required. */\n  children: ReactNode;\n  /** Milliseconds before the first tooltip in the group opens. Defaults to 500. */\n  delay?: number;\n}) {\n  const last = useRef<Last | null>(null);\n  return (\n    <RadixTooltip.Provider delayDuration={delay} skipDelayDuration={SKIP}>\n      <Grouped.Provider value={last}>{children}</Grouped.Provider>\n    </RadixTooltip.Provider>\n  );\n}\n\nexport interface TooltipClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the trigger part; no override by default. */\n  trigger?: string;\n  /** Classes for the shortcut part; no override by default. */\n  shortcut?: string;\n}\n\nexport interface TooltipProps {\n  /** What the control does; a few words. */\n  content: ReactNode;\n  /** A keyboard shortcut shown after the text, e.g. \"⌘B\". */\n  shortcut?: string;\n  /** The control: one element that can take a ref (a button). */\n  children: ReactElement;\n  /** Edge from which the panel or anchored content opens. Defaults to \"top\". */\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  /** Alignment within the available space. Defaults to \"center\". */\n  align?: \"start\" | \"center\" | \"end\";\n  /** Open state, controlled or uncontrolled; the tooltip opens on hover and on keyboard focus either way. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. Defaults to false. */\n  defaultOpen?: boolean;\n  /** Reports open state after a change; no callback by default. */\n  onOpenChange?: (open: boolean) => void;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: TooltipClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * Short supporting text for a control: it opens on hover after a moment and at once on keyboard focus, and closes on\n * Escape. It arrives out of its control on whichever side it opens. Inside a TooltipGroup the next one opens instantly\n * and travels from where the last one stood. Never put essential information or anything clickable in a tooltip.\n */\nexport function Tooltip({\n  content,\n  shortcut,\n  children,\n  side = \"top\",\n  align: alignProp,\n  open,\n  defaultOpen,\n  onOpenChange,\n  className,\n  classNames,\n  style,\n  ref,\n}: TooltipProps) {\n  const scopeDefaults = useComponentDefaults<TooltipDefaults>(\"tooltip\", [\"align\"]);\n  const align = alignProp ?? scopeDefaults.align ?? \"center\";\n\n  const group = useContext(Grouped);\n  const container = usePortalContainer();\n  const trigger = useRef<HTMLButtonElement>(null); // the control, whatever element it is (Radix types it as a button)\n  // in a group, how far this tooltip is from the last one, so it can start there and travel to its own control\n  const [travel, setTravel] = useState<{ x: number; y: number } | null>(null);\n  const change = (next: boolean) => {\n    const el = trigger.current;\n    if (group && el) {\n      const r = el.getBoundingClientRect();\n      const x = r.left + r.width / 2;\n      const y = r.top + r.height / 2;\n      const last = group.current;\n      if (next) {\n        const near = last && last.el !== el && performance.now() - last.at < SKIP;\n        setTravel(near ? { x: last.x - x, y: last.y - y } : null);\n        group.current = { el, x, y, at: Infinity };\n      } else if (last?.el === el) last.at = performance.now();\n    }\n    onOpenChange?.(next);\n  };\n  const tip = (\n    <RadixTooltip.Root open={open} defaultOpen={defaultOpen} onOpenChange={change}>\n      <RadixTooltip.Trigger ref={trigger} asChild data-slot=\"tooltip-trigger\" className={classNames?.trigger}>\n        {children}\n      </RadixTooltip.Trigger>\n      {container !== null && (\n        <RadixTooltip.Portal container={container}>\n          <RadixTooltip.Content\n            data-slot=\"tooltip\"\n            data-grouped={group ? \"\" : undefined}\n            data-travel={travel ? \"\" : undefined}\n            style={travel ? ({ \"--_tx\": `${travel.x}px`, \"--_ty\": `${travel.y}px`, ...style } as CSSProperties) : style}\n            ref={ref}\n            side={side}\n            align={align}\n            sideOffset={6}\n            collisionPadding={8}\n            className={cn(\n              \"flex max-w-[min(260px,calc(100vw-16px))] items-center gap-2 rounded-chip bg-tooltip px-2 py-1 text-xs leading-snug text-tooltip-fg shadow-overlay\",\n              styles.tip,\n              classNames?.root,\n              className,\n            )}\n          >\n            {content}\n            {shortcut && (\n              <kbd\n                data-slot=\"tooltip-shortcut\"\n                className={cn(\"font-mono text-2xs text-[var(--pui-tooltip-muted)]\", classNames?.shortcut)}\n              >\n                {shortcut}\n              </kbd>\n            )}\n          </RadixTooltip.Content>\n        </RadixTooltip.Portal>\n      )}\n    </RadixTooltip.Root>\n  );\n  return group ? tip : <RadixTooltip.Provider delayDuration={500}>{tip}</RadixTooltip.Provider>;\n}\n\nexport default Tooltip;\n"},{"path":"beamline/hooks/use-active.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-active.ts","content":"\"use client\";\n\nimport { useContext, useEffect, useState, type RefObject } from \"react\";\n\nimport { ActivityContext } from \"../runtime/activity\";\n\n/**\n * Whether an animated element should be doing work: it is (nearly) on screen and the tab is visible. Every\n * continuously animated component gates its loop, shader or CSS animation on this, so an effect scrolled out of view,\n * behind another tab, or in a collapsed panel costs nothing. One IntersectionObserver per element; no polling.\n */\nexport function useActive(\n  ref: RefObject<Element | null>,\n  {\n    margin = \"120px\",\n    key,\n    idle = true,\n  }: {\n    margin?: string;\n    /** Re-attach when this changes (an element that mounts later). */\n    key?: unknown;\n    /** Pause continuous work at the host idle boundary. Finite sequences set false so they finish. */\n    idle?: boolean;\n  } = {},\n) {\n  const active = useContext(ActivityContext);\n  const [inView, setInView] = useState(false);\n  const [visible, setVisible] = useState(\n    () => typeof document === \"undefined\" || document.visibilityState === \"visible\",\n  );\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") {\n      setInView(true);\n      return;\n    }\n    const io = new IntersectionObserver(([e]) => setInView(e.isIntersecting), { rootMargin: margin });\n    io.observe(el);\n    return () => io.disconnect();\n  }, [ref, margin, key]);\n  useEffect(() => {\n    const on = () => setVisible(document.visibilityState === \"visible\");\n    document.addEventListener(\"visibilitychange\", on);\n    return () => document.removeEventListener(\"visibilitychange\", on);\n  }, []);\n  return (!idle || active) && inView && visible;\n}\n"},{"path":"beamline/hooks/use-composed-ref.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-composed-ref.ts","content":"\"use client\";\n\nimport { useCallback, type Ref, type RefObject } from \"react\";\n\n/** Keep a control's measurement/focus ref and the caller's ref, including React 19 callback cleanup. */\nexport function useComposedRef<T>(internal: RefObject<T | null>, external?: Ref<T>) {\n  return useCallback(\n    (node: T | null) => {\n      internal.current = node;\n      if (typeof external === \"function\") {\n        const cleanup = external(node);\n        if (typeof cleanup === \"function\")\n          return () => {\n            internal.current = null;\n            cleanup();\n          };\n      } else if (external) external.current = node;\n    },\n    [internal, external],\n  );\n}\n"},{"path":"beamline/hooks/use-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-contour.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-contour.ts","content":"\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef } from \"react\";\nimport { createContour, type Contour } from \"../lib/contour\";\nimport type { IndicatorBox } from \"./use-indicator\";\n\n/**\n * Puts a decorative surface (a segmented thumb, a tab's pill or line) on the contour engine and moves it to `box`\n * whenever the box changes: the first placement is instant, later ones travel as matter (ui/lib/contour.ts). Returns a\n * ref for the surface. The surface stays aria-hidden; the control's own state has already changed by the time it moves.\n */\nexport function useContour<T extends HTMLElement>(box: IndicatorBox | null) {\n  const contour = useRef<Contour | null>(null);\n  const ref = useCallback((el: T | null) => {\n    contour.current?.destroy();\n    contour.current = el ? createContour(el) : null;\n  }, []);\n  useLayoutEffect(() => {\n    // a choice travels; the first placement and a reflow of the same choice are simply there\n    if (box) contour.current?.to(box, { instant: !box.ready || box.cause === \"layout\" });\n  }, [box]);\n  return ref;\n}\n"},{"path":"beamline/hooks/use-controllable.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-controllable.ts","content":"\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\n/**\n * One value in both forms (rules/CONVENTIONS.md): the prop while it is passed, otherwise the element's own copy that\n * starts at `initial` (a value, or a function that makes it once). The setter takes a value or an updater; an updater\n * reads the latest value even twice in one event, and every change reaches `onChange`, controlled or not.\n * `Next` is what a change can carry when it is narrower than the state: a range that starts empty (null) but is only\n * ever changed to a range.\n */\nexport function useControllable<T, Next extends T = T>(\n  value: T | undefined,\n  initial: T | (() => T),\n  onChange?: (next: Next) => void,\n) {\n  const [own, setOwn] = useState<T>(initial);\n  const current = value !== undefined ? value : own;\n  const latest = useRef(current);\n  latest.current = current;\n  const set = useCallback(\n    (next: Next | ((was: T) => Next)) => {\n      const resolved = typeof next === \"function\" ? (next as (was: T) => Next)(latest.current) : next;\n      latest.current = resolved;\n      if (value === undefined) setOwn(resolved);\n      onChange?.(resolved);\n    },\n    [value, onChange],\n  );\n  return [current, set] as const;\n}\n"},{"path":"beamline/hooks/use-deprecated-prop.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-deprecated-prop.ts","content":"\"use client\";\n\nimport { useEffect } from \"react\";\n\n// Bundlers replace NODE_ENV for browser builds; this declaration emits no Node dependency.\ndeclare const process: { env: { NODE_ENV?: string } };\n\nconst warned = new Set<string>();\n\n/** Report a legacy prop once after mount in development; production and server renders stay silent. */\nexport function useDeprecatedProp(component: string, prop: string, replacement: string, supplied: boolean) {\n  useEffect(() => {\n    if (process.env.NODE_ENV !== \"development\" || !supplied) return;\n    const key = `${component}.${prop}`;\n    if (warned.has(key)) return;\n    warned.add(key);\n    console.warn(`[Beamline] ${key} is deprecated. Use ${replacement} instead; the legacy prop still works.`);\n  }, [component, prop, replacement, supplied]);\n}\n"},{"path":"beamline/hooks/use-drag.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-drag.ts","content":"\"use client\";\n\nimport { useEffect, useRef, useState, type RefObject } from \"react\";\nimport { VelocityTracker } from \"../lib/gesture\";\n\n/** Where the pointer is during a drag, relative to where it was pressed, with its velocity (px/s) over the last 100 ms. */\nexport interface DragPoint {\n  /** Pointer position in client coordinates. */\n  x: number;\n  y: number;\n  /** Distance from the press. */\n  dx: number;\n  dy: number;\n  /** Velocity in px/s (read from the last 100 ms, so a release carries the flick, not the whole drag). */\n  vx: number;\n  vy: number;\n  pointerType: string;\n  /** Shift was held (a part may offer a second mode on it: move one task alone, a finer step). */\n  shift: boolean;\n}\n\nexport interface DragHandlers {\n  /** A press landed on the element. Return false to leave it alone (a press on a nested button or link). */\n  onPress?: (point: DragPoint, event: PointerEvent) => boolean | void;\n  /** The pointer moved past the threshold: the drag begins (the press point is in `point.x - point.dx`). */\n  onStart?: (point: DragPoint) => void;\n  /** The pointer moved during a drag. `event` is the browser's own, for what a point does not carry: a pen's pressure,\n   *  the coalesced samples a fast stroke packs into one event (a drawing surface). */\n  onMove?: (point: DragPoint, event: PointerEvent) => void;\n  /** Released after a drag, with the release velocity. */\n  onEnd?: (point: DragPoint) => void;\n  /** Released without moving past the threshold: a tap or click (the part's tap route, WCAG 2.5.7). */\n  onTap?: (point: DragPoint) => void;\n  /** Escape, a lost pointer or the browser taking the gesture (a page scroll): put things back as they were. */\n  onCancel?: () => void;\n  /** A press that ended as neither a drag nor a tap: a finger that went on to scroll the page, or a pointer the browser\n   *  took before the threshold. Undo whatever `onPress` prepared. */\n  onAbandon?: () => void;\n}\n\nexport interface DragOptions {\n  /** The axis the part moves on. On touch, a drag that starts mostly across it is left to the page (a scroll). */\n  axis?: \"x\" | \"y\" | \"both\";\n  /** Pixels before a press becomes a drag: 3 for a mouse, 8 for a finger or pen unless given. */\n  threshold?: number;\n  disabled?: boolean;\n}\n\n/**\n * The pointer side of a hand on a part, shared by everything dragged, thrown or pulled (a thumb, a puck, a stub, a\n * task, a cable). It listens natively on the element, so moves never render React: the part writes positions in its\n * handlers (a spring, a transform, a custom property) and only `dragging`, which changes at the start and end, is\n * state (for `data-state`).\n *\n * A press becomes a drag past a small threshold, then the pointer is captured so the drag continues outside the\n * element; a release without moving is a tap (`onTap`), the route every drag needs for people who cannot drag. Escape,\n * `pointercancel` and a lost capture call `onCancel`, and the part restores what it showed before the press. On touch the\n * element needs a `touch-action` that leaves the other axis to the page (`pan-y` for a part dragged sideways; never\n * `none` on more than the dragged surface, which would block zoom), and a drag that starts across its axis is dropped\n * so the page can scroll.\n */\nexport function useDrag<T extends HTMLElement | SVGElement>(\n  ref: RefObject<T | null>,\n  handlers: DragHandlers,\n  { axis = \"both\", threshold, disabled = false }: DragOptions = {},\n): { dragging: boolean } {\n  const [dragging, setDragging] = useState(false);\n  const latest = useRef(handlers);\n  latest.current = handlers;\n\n  useEffect(() => {\n    // an SVG part (a chart's brush) is dragged the same way; its pointer events are typed as an HTML element's\n    const el = ref.current as HTMLElement | null;\n    if (!el || disabled) return;\n    const doc = el.ownerDocument;\n    // Escape during a drag belongs to the drag: listened on the window's capture phase, it runs before any dialog or\n    // popover listening on the document, which would otherwise also close around the cancelled drag\n    const win = doc.defaultView ?? window;\n    let active: {\n      id: number;\n      x0: number;\n      y0: number;\n      type: string;\n      started: boolean;\n      tracker: VelocityTracker;\n    } | null = null;\n\n    const point = (e: PointerEvent): DragPoint => {\n      const a = active!;\n      const [vx, vy] = a.tracker.velocity(e.timeStamp);\n      return {\n        x: e.clientX,\n        y: e.clientY,\n        dx: e.clientX - a.x0,\n        dy: e.clientY - a.y0,\n        vx,\n        vy,\n        pointerType: e.pointerType,\n        shift: e.shiftKey,\n      };\n    };\n\n    const finish = () => {\n      if (active?.started && el.hasPointerCapture?.(active.id)) el.releasePointerCapture(active.id);\n      active = null;\n      doc.removeEventListener(\"pointermove\", move);\n      doc.removeEventListener(\"pointerup\", up);\n      doc.removeEventListener(\"pointercancel\", cancel);\n      win.removeEventListener(\"keydown\", key, true);\n      doc.removeEventListener(\"selectstart\", hold, true);\n      doc.removeEventListener(\"dragstart\", hold, true);\n      el.removeEventListener(\"lostpointercapture\", lost);\n      setDragging(false);\n    };\n\n    const cancel = () => {\n      if (!active) return;\n      const started = active.started;\n      finish();\n      if (started) latest.current.onCancel?.();\n      else latest.current.onAbandon?.();\n    };\n\n    // only the part's own capture ending cancels: a finger's press is captured by the child it landed on, and moving\n    // the capture to the part fires lostpointercapture on that child, which bubbles here\n    const lost = (e: PointerEvent) => {\n      if (e.target === el && active?.started && e.pointerId === active.id) cancel();\n    };\n\n    const key = (e: KeyboardEvent) => {\n      if (e.key !== \"Escape\" || !active?.started) return;\n      e.preventDefault();\n      e.stopImmediatePropagation();\n      cancel();\n    };\n\n    // while the hand holds the part, the page neither selects text nor starts its own drag and drop (a selection left\n    // by an earlier drag would otherwise be dragged, and the browser cancels the pointer)\n    const hold = (e: Event) => e.preventDefault();\n\n    const move = (e: PointerEvent) => {\n      if (!active || e.pointerId !== active.id) return;\n      active.tracker.add(e.timeStamp, e.clientX, e.clientY);\n      const p = point(e);\n      if (!active.started) {\n        const limit = threshold ?? (active.type === \"mouse\" ? 3 : 8);\n        if (Math.hypot(p.dx, p.dy) < limit) return;\n        // a finger that starts across the part's axis is scrolling the page\n        const across = axis === \"x\" ? Math.abs(p.dy) > Math.abs(p.dx) : axis === \"y\" && Math.abs(p.dx) > Math.abs(p.dy);\n        if (active.type !== \"mouse\" && across) {\n          finish();\n          latest.current.onAbandon?.();\n          return;\n        }\n        active.started = true;\n        doc.getSelection()?.removeAllRanges();\n        el.setPointerCapture?.(active.id);\n        el.addEventListener(\"lostpointercapture\", lost);\n        setDragging(true);\n        latest.current.onStart?.(p);\n      }\n      latest.current.onMove?.(p, e);\n    };\n\n    const up = (e: PointerEvent) => {\n      if (!active || e.pointerId !== active.id) return;\n      active.tracker.add(e.timeStamp, e.clientX, e.clientY);\n      const p = point(e);\n      const started = active.started;\n      finish();\n      if (started) latest.current.onEnd?.(p);\n      else latest.current.onTap?.(p);\n    };\n\n    const down = (e: PointerEvent) => {\n      if (active || !e.isPrimary || (e.pointerType === \"mouse\" && e.button !== 0)) return;\n      const tracker = new VelocityTracker();\n      tracker.add(e.timeStamp, e.clientX, e.clientY);\n      active = { id: e.pointerId, x0: e.clientX, y0: e.clientY, type: e.pointerType, started: false, tracker };\n      if (latest.current.onPress?.(point(e), e) === false) {\n        active = null;\n        return;\n      }\n      doc.addEventListener(\"pointermove\", move);\n      doc.addEventListener(\"pointerup\", up);\n      doc.addEventListener(\"pointercancel\", cancel);\n      win.addEventListener(\"keydown\", key, true);\n      doc.addEventListener(\"selectstart\", hold, true);\n      doc.addEventListener(\"dragstart\", hold, true);\n    };\n\n    el.addEventListener(\"pointerdown\", down);\n    return () => {\n      el.removeEventListener(\"pointerdown\", down);\n      if (active) finish();\n    };\n  }, [ref, axis, threshold, disabled]);\n\n  return { dragging };\n}\n"},{"path":"beamline/hooks/use-height-morph.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-height-morph.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, type RefObject } from \"react\";\nimport { useMotionTokens } from \"./use-motion-tokens\";\nimport { cssEase } from \"../lib/motion-tokens\";\n\n/**\n * The step tray: a card whose content is replaced (a sign-in step, the sign-up card, a form step) keeps its path. When\n * `key` changes, the element's height eases from what it was to what the new content needs, so the card's edge and\n * everything under it glide instead of jumping (the morph grammar's \"modes change size in place\"). The element clips\n * while it eases, with room for focus rings, unless `clip` is false (a slot whose ease only moves what is around it, like\n * the auth page re-centring its card, shows its content whole). Nothing runs at rest; reduced motion, motion \"none\" and a card off screen\n * switch at once; a change mid-way starts from where the card is. The height twin of hooks/use-width-morph; any element whose\n * content is replaced in place uses it (the auth cards, the step form, a dialog's stepped tray, the command menu's pages).\n */\nexport function useHeightMorph(\n  ref: RefObject<HTMLElement | null>,\n  key: unknown,\n  {\n    clip = true,\n    duration = \"morph\",\n    reduced: reducedOverride = false,\n  }: {\n    clip?: boolean;\n    /** The semantic duration the ease takes: a mode change morphs, a content swap (a skeleton) takes the standard. */\n    duration?: \"morph\" | \"standard\";\n    /** Also place at once when the caller knows motion is reduced by its own policy. */\n    reduced?: boolean;\n  } = {},\n) {\n  const motion = useMotionTokens(ref);\n  const last = useRef<number | null>(null);\n  const easing = useRef<Animation | null>(null);\n  // the height it last rested at, kept current when it changes on its own (an error line, a bot check) so the next ease\n  // starts where the card really is; the ease's own frames are not a resting height\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      if (!easing.current) last.current = el.getBoundingClientRect().height;\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref]);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const from = easing.current ? el.getBoundingClientRect().height : last.current;\n    easing.current?.cancel();\n    easing.current = null;\n    const box = el.getBoundingClientRect();\n    const to = box.height;\n    last.current = to;\n    const ms = motion.duration[duration] * 1000;\n    if (from === null || Math.abs(from - to) < 0.5 || motion.reduced || reducedOverride || ms < 2) return;\n    // off screen nobody sees the glide: the card takes its height at once and no frame is spent on it\n    if (box.bottom < 0 || box.top > innerHeight) return;\n    // the ease runs between measured heights: a keyframe to \"auto\" (even with interpolate-size, or calc-size) does not\n    // interpolate in Chromium's Web Animations, it jumps at once; at the end the element is back on its own height\n    // the clip below leaves room for a focus ring or a control's shadow at the content's edge (inert at rest); a plain\n    // length, because Chromium computes any calc() here (and so the space tokens) as 0\n    el.style.setProperty(\"overflow-clip-margin\", \"8px\");\n    const ease = motion.ease.default;\n    const overflow = clip ? \"clip\" : \"visible\";\n    const animation = el.animate(\n      [\n        { height: `${from}px`, overflow },\n        { height: `${to}px`, overflow },\n      ],\n      { duration: ms, easing: cssEase(ease, \"ease-in-out\") },\n    );\n    easing.current = animation;\n    void animation.finished.then(\n      () => {\n        if (easing.current !== animation) return;\n        easing.current = null;\n        last.current = el.getBoundingClientRect().height;\n      },\n      () => {},\n    );\n  }, [key]); // eslint-disable-line react-hooks/exhaustive-deps -- eases on a change of the step it stands for\n}\n"},{"path":"beamline/hooks/use-indicator.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-indicator.ts","content":"\"use client\";\n\nimport { APPEARANCE_EVENT } from \"../lib/motion-tokens\";\nimport { useLayoutEffect, useRef, useState, type CSSProperties, type RefObject } from \"react\";\n\nexport interface IndicatorBox {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  /** False on the first placement, so the indicator appears in place instead of sliding in from the corner. */\n  ready: boolean;\n  /** Which way the last move went on each axis (-1 left or up, 1 right or down, 0 still), for the edge that leads. */\n  dx: -1 | 0 | 1;\n  dy: -1 | 0 | 1;\n  /** choice: a selection changed; layout: the same choice moved because the page reflowed (a resize, a font, a\n   *  type scale). A surface may travel for a choice and should simply be there after a reflow. */\n  cause: \"choice\" | \"layout\";\n}\n\ntype Box = Pick<IndicatorBox, \"x\" | \"y\" | \"w\" | \"h\">;\nconst same = (a: IndicatorBox | null, b: Box) => a !== null && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;\n\n/**\n * Follows the chosen item inside a container: the thumb of a segmented control, the pill or line of tabs, the ring of\n * radio cards. Returns a ref for the container and the box of the first element matching `selector` in it, relative\n * to the container (null when nothing matches). It measures before paint whenever `key` changes, and again when the\n * container resizes or a selection attribute changes inside it (for parts whose choice lives in a library, like tabs).\n */\nexport function useIndicator<T extends HTMLElement>(\n  selector: string,\n  key?: unknown,\n  onPlace?: (item: HTMLElement, container: T) => void,\n  /** offset (default): the item's offset box, for items whose offset parent is the container. rect: measured on screen\n   *  relative to the container, for items nested in positioned cells (a calendar day in its table cell). */\n  measure: \"offset\" | \"rect\" = \"offset\",\n): [RefObject<T | null>, IndicatorBox | null] {\n  const ref = useRef<T>(null);\n  const [box, setBox] = useState<IndicatorBox | null>(null);\n  const placed = useRef(onPlace);\n  useLayoutEffect(() => {\n    placed.current = onPlace;\n  });\n  useLayoutEffect(() => {\n    const container = ref.current;\n    if (!container) return;\n    const place = (cause: IndicatorBox[\"cause\"] = \"choice\") => {\n      const item = container.querySelector<HTMLElement>(selector);\n      if (!item) {\n        setBox(null);\n        return;\n      }\n      let next = { x: item.offsetLeft, y: item.offsetTop, w: item.offsetWidth, h: item.offsetHeight };\n      if (measure === \"rect\") {\n        const a = item.getBoundingClientRect();\n        const c = container.getBoundingClientRect();\n        next = {\n          x: Math.round(a.left - c.left - container.clientLeft + container.scrollLeft),\n          y: Math.round(a.top - c.top - container.clientTop + container.scrollTop),\n          w: item.offsetWidth,\n          h: item.offsetHeight,\n        };\n      }\n      setBox((b) =>\n        same(b, next)\n          ? b\n          : {\n              ...next,\n              cause,\n              ready: b !== null,\n              dx: b ? (Math.sign(next.x - b.x) as IndicatorBox[\"dx\"]) : 0,\n              dy: b ? (Math.sign(next.y - b.y) as IndicatorBox[\"dy\"]) : 0,\n            },\n      );\n      placed.current?.(item, container);\n    };\n    const resizes = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(() => place(\"layout\"));\n    resizes?.observe(container);\n    const observed = new Set<Element>();\n    const observeItems = () => {\n      // A full-width fixed-height list can stay the same size while its tabs/radio choices grow with type or labels.\n      // Ignore the decorative indicator itself, whose own dimensions are the output of this measurement.\n      const items = new Set([...container.children].filter((child) => child.getAttribute(\"aria-hidden\") !== \"true\"));\n      for (const old of observed)\n        if (!items.has(old)) {\n          resizes?.unobserve(old);\n          observed.delete(old);\n        }\n      for (const item of items)\n        if (!observed.has(item)) {\n          resizes?.observe(item);\n          observed.add(item);\n        }\n    };\n    observeItems();\n    place();\n    const mutations = new MutationObserver(() => {\n      observeItems();\n      place(\"choice\");\n    });\n    mutations.observe(container, {\n      subtree: true,\n      childList: true,\n      attributes: true,\n      attributeFilter: [\"data-state\", \"aria-checked\", \"aria-selected\", \"aria-current\", \"data-active\", \"data-open\"],\n    });\n    const reflow = () => place(\"layout\");\n    container.ownerDocument.addEventListener(APPEARANCE_EVENT, reflow);\n    container.ownerDocument.fonts?.addEventListener(\"loadingdone\", reflow);\n    return () => {\n      mutations.disconnect();\n      resizes?.disconnect();\n      container.ownerDocument.removeEventListener(APPEARANCE_EVENT, reflow);\n      container.ownerDocument.fonts?.removeEventListener(\"loadingdone\", reflow);\n    };\n  }, [selector, key, measure]);\n  return [ref, box];\n}\n\n/** The box as custom properties, for a stylesheet to place the indicator with: --pui-indicator-x/-y/-w/-h, and its\n *  four edges --pui-glide-l/-r/-t/-b, which the `glide` class moves one at a time. */\nexport function indicatorStyle(box: IndicatorBox): CSSProperties {\n  return {\n    \"--pui-indicator-x\": `${box.x}px`,\n    \"--pui-indicator-y\": `${box.y}px`,\n    \"--pui-indicator-w\": `${box.w}px`,\n    \"--pui-indicator-h\": `${box.h}px`,\n    \"--pui-glide-l\": `${box.x}px`,\n    \"--pui-glide-r\": `${box.x + box.w}px`,\n    \"--pui-glide-t\": `${box.y}px`,\n    \"--pui-glide-b\": `${box.y + box.h}px`,\n  } as CSSProperties;\n}\n\nconst AXIS = { x: [\"left\", undefined, \"right\"], y: [\"up\", undefined, \"down\"] } as const;\n\n/** Everything an indicator wearing the `glide` class needs (utilities.css): its place, whether it may move yet, and\n *  which way it is going, so the edge on that side leads and the other follows: it stretches toward the new choice and\n *  gathers there. */\nexport function glideProps(box: IndicatorBox) {\n  return {\n    style: indicatorStyle(box),\n    \"data-ready\": box.ready ? \"\" : undefined,\n    \"data-glide-x\": AXIS.x[box.dx + 1],\n    \"data-glide-y\": AXIS.y[box.dy + 1],\n  };\n}\n"},{"path":"beamline/hooks/use-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/hooks/use-width-morph.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-width-morph.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, type RefObject } from \"react\";\nimport { useMotionTokens } from \"./use-motion-tokens\";\nimport { cssEase } from \"../lib/motion-tokens\";\n\n/**\n * A label whose text changes keeps its path: when `key` changes, the element's width eases from what it was to what the\n * new text needs, so whatever sits after it (a chevron, a counter) glides instead of jumping. The element clips while it\n * eases (give it `overflow-clip whitespace-nowrap`). Nothing runs at rest; reduced motion and motion \"none\" switch at\n * once, and so does a label off screen.\n */\nexport function useWidthMorph(ref: RefObject<HTMLElement | null>, key: unknown) {\n  const motion = useMotionTokens(ref);\n  const last = useRef<number | null>(null);\n  const easing = useRef<Animation | null>(null);\n  // the width it last rested at, kept current when it changes on its own (a web font arriving) so the next ease starts\n  // where the label really is; the ease's own frames are not a resting width\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      if (!easing.current) last.current = el.getBoundingClientRect().width;\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref]);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    // mid-way through an earlier change, it starts from where it is now, not from where that change was going\n    const from = easing.current ? el.getBoundingClientRect().width : last.current;\n    easing.current?.cancel();\n    easing.current = null;\n    const box = el.getBoundingClientRect();\n    const to = box.width;\n    last.current = to;\n    const ms = motion.duration.morph * 1000;\n    if (from === null || Math.abs(from - to) < 0.5 || motion.reduced || ms < 2) return;\n    // off screen nobody sees the glide: the label takes its width at once and no frame is spent on it\n    if (box.bottom < 0 || box.top > innerHeight || box.right < 0 || box.left > innerWidth) return;\n    const ease = motion.ease.default;\n    // pixel to pixel: a Web Animations keyframe to \"auto\" (with interpolate-size, or calc-size) jumps straight to the\n    // end in Chromium 153 instead of easing (measured: 537 of 537 px at 300 of 1000 ms); a late change of the natural\n    // width is caught by the observer above when the ease ends\n    const animation = el.animate([{ width: `${from}px` }, { width: `${to}px` }], {\n      duration: ms,\n      // even in and out: the glide never moves a neighbour more than a pixel or two in one frame\n      easing: cssEase(ease, \"ease-in-out\"),\n    });\n    easing.current = animation;\n    void animation.finished.then(\n      () => {\n        if (easing.current !== animation) return;\n        easing.current = null;\n        last.current = el.getBoundingClientRect().width;\n      },\n      () => {},\n    );\n  }, [key]); // eslint-disable-line react-hooks/exhaustive-deps -- eases on a change of the text it stands for\n}\n"},{"path":"beamline/lib/contour.ts","type":"registry:file","target":"src/ui/beamline/lib/contour.ts","content":"import { readMotionFactor } from \"./motion-tokens\";\n\n/**\n * Contour: a surface whose outline moves like matter instead of a box that resizes. Beamline's own model (research/\n * morphing-interfaces-2026-10-04): a rounded quadrilateral whose four corners each follow their target on a critically\n * damped spring. The corner facing the way it travels answers fastest and the far one slowest, so it leads and gathers;\n * each edge's midpoint heads for where its edge will be a little ahead of its corners and may only bulge outward (the\n * leading edge rounds out like the front of a drop, the tail stays smooth), and while the shape is longer than its target the long edges\n * pinch toward the centre, so a fast move draws a neck; while it is stretched its corners round toward a capsule, as a moving drop does. No spring overshoots: the elasticity is the parts arriving at\n * different moments, never bounce. At rest the surface is its plain CSS box with no clip and no frames.\n *\n * The look decides the material through four inputs on the element (preset-source.css): --m-morph-spread (how unevenly\n * the corners answer; 0 is rigid travel), --m-morph-front (px the leading edge may round out), --m-morph-waist (how much\n * a stretch pinches) and --m-morph-weight (a multiplier on the response time).\n */\nexport interface ContourBox {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n}\n\nexport interface Contour {\n  /** Move the surface to `box` (in its offset parent's coordinates). `instant` places it with no motion. */\n  to(box: ContourBox, options?: { instant?: boolean }): void;\n  /** Stop and leave the surface at its target. */\n  destroy(): void;\n}\n\ntype Point = [number, number];\ntype Corner = \"tl\" | \"tr\" | \"br\" | \"bl\";\ntype Edge = \"t\" | \"r\" | \"b\" | \"l\";\nconst CORNERS: Corner[] = [\"tl\", \"tr\", \"br\", \"bl\"];\nconst EDGES: Edge[] = [\"t\", \"r\", \"b\", \"l\"];\n/** Outward direction of each corner, for deciding which one leads. */\nconst OUT: Record<Corner, Point> = { tl: [-1, -1], tr: [1, -1], br: [1, 1], bl: [-1, 1] };\n/** Outward direction of each edge. */\nconst NORMAL: Record<Edge, Point> = { t: [0, -1], r: [1, 0], b: [0, 1], l: [-1, 0] };\n/** Settles at the motion-token morph spring at the foundation default. */\nconst BASE_RESPONSE = 0.26;\n\ninterface Mechanics {\n  spread: number;\n  front: number;\n  waist: number;\n  weight: number;\n  /** The colour of the edge drawn along a non-rectangular outline (the look's hairline, which a clip would cut off). */\n  rim: string;\n  rimWidth: number;\n}\n\n/** One step of a critically damped spring with response time `t` (s), in closed form so a long frame cannot explode. */\nfunction step(x: number, v: number, target: number, t: number, dt: number): [number, number] {\n  const w = (2 * Math.PI) / (t * 1.2);\n  const d = x - target;\n  const e = Math.exp(-w * dt);\n  return [target + (d + (v + w * d) * dt) * e, (v - w * (v + w * d) * dt) * e];\n}\n\nconst cornersOf = (b: ContourBox): Record<Corner, Point> => ({\n  tl: [b.x, b.y],\n  tr: [b.x + b.w, b.y],\n  br: [b.x + b.w, b.y + b.h],\n  bl: [b.x, b.y + b.h],\n});\nconst midsOf = (c: Record<Corner, Point>): Record<Edge, Point> => ({\n  t: [(c.tl[0] + c.tr[0]) / 2, (c.tl[1] + c.tr[1]) / 2],\n  r: [(c.tr[0] + c.br[0]) / 2, (c.tr[1] + c.br[1]) / 2],\n  b: [(c.br[0] + c.bl[0]) / 2, (c.br[1] + c.bl[1]) / 2],\n  l: [(c.bl[0] + c.tl[0]) / 2, (c.bl[1] + c.tl[1]) / 2],\n});\nconst zero = <K extends string>(keys: K[]) => Object.fromEntries(keys.map((k) => [k, [0, 0]])) as Record<K, Point>;\nconst num = (style: CSSStyleDeclaration, name: string, fallback: number) => {\n  const value = Number.parseFloat(style.getPropertyValue(name));\n  return Number.isFinite(value) ? value : fallback;\n};\n\n/** All moving contours share one frame loop, which stops when the last one settles. */\nconst moving = new Set<(dt: number) => boolean>();\nlet frame = 0;\nlet last = 0;\nfunction tick(now: number) {\n  const dt = Math.min(0.032, Math.max(0.001, (now - last) / 1000));\n  last = now;\n  for (const advance of moving) if (!advance(dt)) moving.delete(advance);\n  frame = moving.size ? requestAnimationFrame(tick) : 0;\n}\nfunction run(advance: (dt: number) => boolean) {\n  moving.add(advance);\n  if (!frame) {\n    last = performance.now();\n    frame = requestAnimationFrame(tick);\n  }\n}\n\n/** One contour per element, so a remount (or React's development double mount) keeps its place and keeps moving. */\nconst contours = new WeakMap<HTMLElement, Contour>();\nexport function createContour(el: HTMLElement): Contour {\n  const existing = contours.get(el);\n  if (existing) return existing;\n  let target: ContourBox | null = null;\n  let pos: Record<Corner, Point> | null = null;\n  let vel = zero(CORNERS);\n  let mid: Record<Edge, Point> = { t: [0, 0], r: [0, 0], b: [0, 0], l: [0, 0] };\n  let midVel = zero(EDGES);\n  let mech: Mechanics = { spread: 0.35, front: 5, waist: 0.16, weight: 1, rim: \"none\", rimWidth: 1 };\n  let rimPath: SVGPathElement | null = null;\n  const rim = (d: string | null, w = 0, h = 0) => {\n    if (!d || mech.rim === \"none\") {\n      if (rimPath) (rimPath.ownerSVGElement as SVGSVGElement).style.display = \"none\";\n      return;\n    }\n    if (!rimPath) {\n      const ns = \"http://www.w3.org/2000/svg\";\n      const svg = el.ownerDocument.createElementNS(ns, \"svg\");\n      svg.setAttribute(\"aria-hidden\", \"true\");\n      svg.setAttribute(\"data-contour-rim\", \"\");\n      svg.style.cssText = \"position:absolute;left:0;top:0;z-index:2;overflow:visible;pointer-events:none\";\n      rimPath = el.ownerDocument.createElementNS(ns, \"path\");\n      rimPath.setAttribute(\"fill\", \"none\");\n      svg.appendChild(rimPath);\n      el.appendChild(svg);\n    }\n    const svg = rimPath.ownerSVGElement as SVGSVGElement;\n    svg.style.display = \"\";\n    svg.setAttribute(\"width\", String(w));\n    svg.setAttribute(\"height\", String(h));\n    rimPath.setAttribute(\"d\", d);\n    // the clip keeps the inner half of the stroke: an edge of rimWidth inside the outline\n    rimPath.setAttribute(\"stroke-width\", String(mech.rimWidth * 2));\n    rimPath.style.stroke = mech.rim;\n  };\n  let response = BASE_RESPONSE;\n  let radius = 0;\n  let running = false;\n\n  const rest = () => {\n    if (!target) return;\n    el.style.transform = `translate(${target.x}px, ${target.y}px)`;\n    el.style.width = `${target.w}px`;\n    el.style.height = `${target.h}px`;\n    el.style.clipPath = \"\";\n    rim(null);\n    delete el.dataset.morphing;\n  };\n\n  const draw = () => {\n    if (!pos || !target) return;\n    const c = pos;\n    const w = (c.tr[0] - c.tl[0] + c.br[0] - c.bl[0]) / 2;\n    const h = (c.bl[1] - c.tl[1] + c.br[1] - c.tr[1]) / 2;\n    // the long edges pinch while the shape is longer than its target (area kept, roughly)\n    const pinchY = Math.min(h * mech.waist * 0.5, 4, h * Math.max(0, w / target.w - 1) * mech.waist);\n    const pinchX = Math.min(w * mech.waist * 0.5, 4, w * Math.max(0, h / target.h - 1) * mech.waist);\n    const edges: Record<Edge, Point> = {\n      t: [mid.t[0], mid.t[1] + pinchY],\n      b: [mid.b[0], mid.b[1] - pinchY],\n      l: [mid.l[0] + pinchX, mid.l[1]],\n      r: [mid.r[0] - pinchX, mid.r[1]],\n    };\n    // still a rectangle (a rigid look, or a straight move): move the box itself and keep every bit of the look's paint\n    const flat = (a: number, b: number) => Math.abs(a - b) < 0.35;\n    const corners = midsOf(c);\n    if (\n      flat(c.tl[1], c.tr[1]) &&\n      flat(c.bl[1], c.br[1]) &&\n      flat(c.tl[0], c.bl[0]) &&\n      flat(c.tr[0], c.br[0]) &&\n      EDGES.every((k) => flat(edges[k][0], corners[k][0]) && flat(edges[k][1], corners[k][1]))\n    ) {\n      el.style.transform = `translate(${c.tl[0]}px, ${c.tl[1]}px)`;\n      el.style.width = `${c.tr[0] - c.tl[0]}px`;\n      el.style.height = `${c.bl[1] - c.tl[1]}px`;\n      el.style.clipPath = \"\";\n      rim(null);\n      return;\n    }\n    const all = [...CORNERS.map((k) => c[k]), ...EDGES.map((k) => edges[k])];\n    const minX = Math.floor(Math.min(...all.map((p) => p[0]))) - 1;\n    const minY = Math.floor(Math.min(...all.map((p) => p[1]))) - 1;\n    const maxX = Math.ceil(Math.max(...all.map((p) => p[0]))) + 1;\n    const maxY = Math.ceil(Math.max(...all.map((p) => p[1]))) + 1;\n    const local = (p: Point): Point => [p[0] - minX, p[1] - minY];\n    const C = CORNERS.map((k) => local(c[k]));\n    const E = ([\"t\", \"r\", \"b\", \"l\"] as Edge[]).map((k) => local(edges[k]));\n    const len = (a: Point, b: Point) => Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;\n    const toward = (a: Point, b: Point, d: number): Point => {\n      const l = len(a, b);\n      return [a[0] + ((b[0] - a[0]) / l) * d, a[1] + ((b[1] - a[1]) / l) * d];\n    };\n    // each corner backs off its radius along both of its real edges and turns through the corner itself, so a sheared\n    // shape never folds over; each edge bows through its midpoint\n    // a moving drop is round: corners grow toward a capsule while the shape is stretched or far from home\n    const away = Math.max(w / target.w - 1, h / target.h - 1, 0);\n    const round = mech.front > 0 ? Math.min(1, away * 2.5) : 0;\n    const r = radius + (Math.min(w, h) / 2 - radius) * Math.max(0, round);\n    const rr = C.map((p, i) => Math.min(r, len(p, C[(i + 1) % 4]) / 2.2, len(p, C[(i + 3) % 4]) / 2.2));\n    const into = C.map((p, i) => toward(p, C[(i + 3) % 4], rr[i]));\n    const out = C.map((p, i) => toward(p, C[(i + 1) % 4], rr[i]));\n    const through = (a: Point, m: Point, b: Point): Point => [\n      2 * m[0] - (a[0] + b[0]) / 2,\n      2 * m[1] - (a[1] + b[1]) / 2,\n    ];\n    const f = (p: Point) => `${p[0].toFixed(1)} ${p[1].toFixed(1)}`;\n    let d = `M${f(out[0])}`;\n    for (let i = 0; i < 4; i++) {\n      const j = (i + 1) % 4;\n      // an edge the rounded corners have nearly used up can only bow a little, or its middle would point\n      const a = out[i];\n      const b = into[j];\n      const chord: Point = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];\n      const bow: Point = [E[i][0] - chord[0], E[i][1] - chord[1]];\n      const most = len(a, b) * 0.18;\n      const bowLen = Math.hypot(bow[0], bow[1]);\n      const m: Point =\n        bowLen > most ? [chord[0] + (bow[0] / bowLen) * most, chord[1] + (bow[1] / bowLen) * most] : E[i];\n      d += ` Q${f(through(a, m, b))} ${f(b)} Q${f(C[j])} ${f(out[j])}`;\n    }\n    el.style.transform = `translate(${minX}px, ${minY}px)`;\n    el.style.width = `${maxX - minX}px`;\n    el.style.height = `${maxY - minY}px`;\n    el.style.clipPath = `path(\"${d}Z\")`;\n    rim(`${d}Z`, maxX - minX, maxY - minY);\n  };\n\n  const advance = (dt: number) => {\n    if (!pos || !target || !el.isConnected) return (running = false);\n    const goal = cornersOf(target);\n    let busy = false;\n    for (const k of CORNERS) {\n      const p = pos[k];\n      const v = vel[k];\n      const g = goal[k];\n      const dx = g[0] - p[0];\n      const dy = g[1] - p[1];\n      const dist = Math.hypot(dx, dy);\n      // -1 … 1: how squarely this corner faces the way it is going\n      const along = dist > 0.5 ? (OUT[k][0] * dx + OUT[k][1] * dy) / (dist * Math.SQRT2) : 0;\n      const t = response * (1 + mech.spread * 0.4 - mech.spread * along);\n      for (const a of [0, 1] as const) {\n        [p[a], v[a]] = step(p[a], v[a], g[a], t, dt);\n        if (Math.abs(p[a] - g[a]) > 0.2 || Math.abs(v[a]) > 4) busy = true;\n      }\n    }\n    const now = midsOf(pos);\n    const aim = midsOf(goal);\n    for (const k of EDGES) {\n      for (const a of [0, 1] as const)\n        [mid[k][a], midVel[k][a]] = step(mid[k][a], midVel[k][a], aim[k][a], response * 0.75, dt);\n      // the lead is a few pixels at most, and never more than a fraction of the edge or of the shape's depth\n      const across = k === \"t\" || k === \"b\" ? target.h : target.w;\n      const edge =\n        k === \"t\" ? [pos.tl, pos.tr] : k === \"b\" ? [pos.br, pos.bl] : k === \"l\" ? [pos.bl, pos.tl] : [pos.tr, pos.br];\n      const limit = Math.min(\n        mech.front,\n        across * 0.3,\n        Math.hypot(edge[1][0] - edge[0][0], edge[1][1] - edge[0][1]) * 0.15,\n      );\n      // edges only ever bulge outward: the front rounds out, the tail stays smooth instead of curling in\n      let ox = mid[k][0] - now[k][0];\n      let oy = mid[k][1] - now[k][1];\n      const [nx, ny] = NORMAL[k];\n      const inward = ox * nx + oy * ny;\n      if (inward < 0) {\n        ox -= inward * nx;\n        oy -= inward * ny;\n        mid[k][0] = now[k][0] + ox;\n        mid[k][1] = now[k][1] + oy;\n      }\n      const off = Math.hypot(ox, oy);\n      if (off > limit) {\n        mid[k][0] = now[k][0] + (ox / off) * limit;\n        mid[k][1] = now[k][1] + (oy / off) * limit;\n      }\n      if (Math.abs(mid[k][0] - aim[k][0]) > 0.2 || Math.abs(mid[k][1] - aim[k][1]) > 0.2) busy = true;\n    }\n    if (!busy) {\n      pos = goal;\n      vel = zero(CORNERS);\n      mid = midsOf(goal);\n      midVel = zero(EDGES);\n      rest();\n      return (running = false);\n    }\n    draw();\n    return true;\n  };\n\n  const contour: Contour = {\n    to(box, options) {\n      const same = target && target.x === box.x && target.y === box.y && target.w === box.w && target.h === box.h;\n      if (same && !options?.instant) return;\n      target = { ...box };\n      const style = getComputedStyle(el);\n      const r = style.borderTopLeftRadius;\n      radius = r.endsWith(\"%\") ? (Number.parseFloat(r) / 100) * Math.min(box.w, box.h) : Number.parseFloat(r) || 0;\n      mech = {\n        spread: num(style, \"--m-morph-spread\", 0.35),\n        front: num(style, \"--m-morph-front\", 6),\n        waist: num(style, \"--m-morph-waist\", 0.18),\n        weight: num(style, \"--m-morph-weight\", 1),\n        rim: style.getPropertyValue(\"--m-morph-rim\").trim() || \"none\",\n        rimWidth: num(style, \"--m-morph-rim-width\", 1),\n      };\n      const factor = readMotionFactor(el);\n      response = BASE_RESPONSE * mech.weight * factor;\n      // first placement, reduced motion, or a hidden surface: be there, with nothing to watch\n      if (!pos || options?.instant || factor < 0.05 || !el.getClientRects().length) {\n        pos = cornersOf(target);\n        vel = zero(CORNERS);\n        mid = midsOf(pos);\n        midVel = zero(EDGES);\n        moving.delete(advance);\n        running = false;\n        rest();\n        return;\n      }\n      el.dataset.morphing = \"\";\n      if (!running) {\n        running = true;\n        run(advance);\n      }\n    },\n    destroy() {\n      moving.delete(advance);\n      running = false;\n      if (target) rest();\n    },\n  };\n  contours.set(el, contour);\n  return contour;\n}\n"},{"path":"beamline/lib/focus.ts","type":"registry:file","target":"src/ui/beamline/lib/focus.ts","content":"/**\n * Keeping a keyboard user's place when something disappears. Removing the element that has focus (a dismissed alert,\n * a removed chip or file, a deleted row) drops focus on <body>, and the next Tab starts at the top of the page.\n */\n\nconst TABBABLE = \"a[href], button, input:not([type=hidden]), select, textarea, [tabindex], [contenteditable=true]\";\n\nfunction tabbables(scope: Element) {\n  // a modal layer (menu, dialog) marks the rest of the page aria-hidden while it is open (data-aria-hidden): that\n  // content is still where focus should land once the layer closes\n  return [...scope.querySelectorAll<HTMLElement>(TABBABLE)].filter(\n    (e) =>\n      e.tabIndex >= 0 &&\n      !e.matches(\":disabled\") &&\n      !e.closest(\"[inert], [aria-hidden=true]:not([data-aria-hidden])\") &&\n      e.getClientRects().length > 0,\n  );\n}\n\n/** The control after `item` in reading order (outside it), or the one before it. */\nfunction neighbour(item: Element, scope: Element) {\n  const all = tabbables(scope).filter((e) => !item.contains(e));\n  return (\n    all.find((e) => item.compareDocumentPosition(e) & Node.DOCUMENT_POSITION_FOLLOWING) ??\n    all.reverse().find((e) => item.compareDocumentPosition(e) & Node.DOCUMENT_POSITION_PRECEDING) ??\n    null\n  );\n}\n\n/**\n * Call just before `item` is removed or hidden: if focus is inside it, focus moves to the next control after it (or\n * the one before). `scope` keeps the search inside a widget (a toolbar, a field); `fallback` is used when the scope has\n * nothing else to focus. Does nothing when focus is elsewhere, so a removal the user did not start never steals focus.\n */\nexport function focusAfterRemoval(\n  item: Element | null | undefined,\n  { scope, fallback }: { scope?: Element | null; fallback?: HTMLElement | null } = {},\n) {\n  if (!item || !item.contains(document.activeElement)) return;\n  (neighbour(item, scope ?? document.body) ?? fallback)?.focus();\n}\n\n/**\n * For an action that may or may not remove its item (the app decides, as with a row's Delete): call before running it.\n * If the item is gone afterwards and focus fell to the page, focus moves to the control that followed it.\n */\nexport function keepPlaceIfRemoved(item: Element | null | undefined, { scope }: { scope?: Element | null } = {}) {\n  if (!item) return;\n  const target = neighbour(item, scope ?? document.body);\n  const check = () => {\n    if (\n      !item.isConnected &&\n      (!document.activeElement || document.activeElement === document.body) &&\n      target?.isConnected\n    )\n      target.focus();\n  };\n  setTimeout(check, 0);\n  setTimeout(check, 400); // after an exit animation\n}\n\n/**\n * A press on a part's surface that hands focus to its control (a slider's well, a mark under its track): call from the\n * pointerdown. Focus goes to `control` without the keyboard's ring (`focusVisible: false`, so the part does not switch\n * to its keyboard look after a click). A mouse press is also followed by the browser's own focus step, which moves\n * focus to the nearest focusable ancestor or the page and leaves the arrow keys dead after the click; that step is\n * cancelled for this one press (and the press selects no text). Cancelling the pointerdown instead would also hide the\n * click from the browser, which then reads the focus as the keyboard's. A control a keyboard user already focused keeps\n * its ring.\n */\nexport function focusFromPress(\n  control: HTMLElement | null | undefined,\n  event: PointerEvent | { nativeEvent: PointerEvent },\n) {\n  if (!control) return;\n  const pointer = \"nativeEvent\" in event ? event.nativeEvent : event;\n  // asserted, not declared globally: shipped source must type-check without this repo's globals.d.ts, on the\n  // TypeScript an app already has (create-next-app installs 5.9, whose DOM lib lacks focusVisible)\n  control.focus({ preventScroll: true, focusVisible: false } as FocusOptions);\n  // the mouse's compatibility events follow the pointerdown in the same task; a finger's arrive only after a tap\n  if (pointer.pointerType !== \"mouse\") return;\n  const cancel = (e: MouseEvent) => e.preventDefault();\n  window.addEventListener(\"mousedown\", cancel, { capture: true, once: true });\n  setTimeout(() => window.removeEventListener(\"mousedown\", cancel, { capture: true }), 0);\n}\n"},{"path":"beamline/lib/gesture.ts","type":"registry:file","target":"src/ui/beamline/lib/gesture.ts","content":"import { readMotionFactor } from \"./motion-tokens\";\n\n/**\n * The arithmetic of a hand on a part, shared by every part that is dragged, thrown or pulled (useDrag is the pointer\n * side). Three pieces, each from how real interfaces feel (Apple, \"Designing Fluid Interfaces\", WWDC 2018):\n *\n * - velocity: read from the last ~100 ms of movement, so the release carries the speed of the flick, not of the whole drag;\n * - projection: where a thrown thing would come to rest if it slid on with the look's carry (`project`), so a part\n *   can decide on release (the nearest value, step or preset to that point) instead of after an animation;\n * - give: past a limit the part follows the hand less and less (`rubberband`) instead of stopping dead, and springs back.\n *\n * How much a part carries and gives is the look's material, read once per gesture from the element (`readHand`):\n * `--m-hand-carry`, `--m-hand-give` and `--m-hand-bounce` (preset-source.css), with `--m-morph-weight` scaling how\n * long things take to settle (times a motion token's duration, which carries the speed setting). Reduced motion\n * carries nothing, gives nothing and never bounces.\n */\n\nexport interface Hand {\n  /** How far a release carries: 0 stops where the hand let go, 1 slides on freely (see `project`). */\n  carry: number;\n  /** How much a part follows the hand past a limit: 0 is a hard stop, 0.55 a phone's rubber band. */\n  give: number;\n  /** Bounce of a released held object (0 critically damped). Only for what the pointer held. */\n  bounce: number;\n  /** The look's weight (`--m-morph-weight`): multiply a motion token's duration by it. The token already carries the\n   *  speed setting, so the weight never does. */\n  weight: number;\n  /** Reduced or no motion: land at once. */\n  reduced: boolean;\n}\n\n/** The hand the defaults describe; also what a server or a test without styles gets. */\nexport const DEFAULT_HAND: Hand = { carry: 0.8, give: 0.45, bounce: 0.14, weight: 1, reduced: false };\n\nconst num = (style: CSSStyleDeclaration, name: string, fallback: number) => {\n  const v = Number.parseFloat(style.getPropertyValue(name));\n  return Number.isFinite(v) ? v : fallback;\n};\n\n/** The look's physics at `el`, read once at the start of a gesture (never per frame). */\nexport function readHand(el: Element | null | undefined): Hand {\n  if (!el || typeof getComputedStyle === \"undefined\") return DEFAULT_HAND;\n  const style = getComputedStyle(el);\n  const factor = readMotionFactor(el);\n  // the motion factor is ~0.005 under reduced motion and 1 at the foundation default\n  const reduced = factor < 0.05;\n  if (reduced) return { carry: 0, give: 0, bounce: 0, weight: 1, reduced: true };\n  return {\n    carry: num(style, \"--m-hand-carry\", DEFAULT_HAND.carry),\n    give: num(style, \"--m-hand-give\", DEFAULT_HAND.give),\n    bounce: num(style, \"--m-hand-bounce\", DEFAULT_HAND.bounce),\n    weight: num(style, \"--m-morph-weight\", 1),\n    reduced: false,\n  };\n}\n\n/** Samples of a pointer's path, kept for the last `window` ms, to read its velocity at release. */\nexport class VelocityTracker {\n  private samples: { t: number; x: number; y: number }[] = [];\n  // a declared field, not a parameter property: shipped source must also compile under erasableSyntaxOnly (Vite's\n  // TypeScript template turns it on), where TypeScript-only syntax with runtime meaning is refused\n  private readonly window: number;\n  constructor(window = 100) {\n    this.window = window;\n  }\n\n  add(t: number, x: number, y: number) {\n    this.samples.push({ t, x, y });\n    const from = t - this.window;\n    while (this.samples.length > 2 && this.samples[0].t < from) this.samples.shift();\n  }\n\n  /** Units per second over the kept window; 0 when the hand stopped before letting go (a pause of more than the window). */\n  velocity(now = this.samples.at(-1)?.t ?? 0): [number, number] {\n    const s = this.samples.filter((p) => p.t >= now - this.window);\n    if (s.length < 2) return [0, 0];\n    // least squares slope over the window: one jittery last sample does not decide the throw\n    const n = s.length;\n    const mt = s.reduce((a, p) => a + p.t, 0) / n;\n    const mx = s.reduce((a, p) => a + p.x, 0) / n;\n    const my = s.reduce((a, p) => a + p.y, 0) / n;\n    let tt = 0;\n    let tx = 0;\n    let ty = 0;\n    for (const p of s) {\n      tt += (p.t - mt) ** 2;\n      tx += (p.t - mt) * (p.x - mx);\n      ty += (p.t - mt) * (p.y - my);\n    }\n    if (tt === 0) return [0, 0];\n    return [(tx / tt) * 1000, (ty / tt) * 1000];\n  }\n\n  reset() {\n    this.samples = [];\n  }\n}\n\n/**\n * How far something released at `velocity` (units/s) slides before friction stops it, with the look's carry. `rate` is\n * the speed kept per millisecond: 0.99 (the default) suits controls on a short scale (a thumb, a puck, a cut), where a\n * flick should carry a little way; 0.998 is a phone list's deceleration, for long scrolling content.\n */\nexport function project(velocity: number, carry: number, rate = 0.99): number {\n  if (carry <= 0) return 0;\n  return (((velocity / 1000) * rate) / (1 - rate)) * carry;\n}\n\n/**\n * Where a part drawn `overshoot` units past its limit sits, given the room `extent` it moves in: it follows less and\n * less the further it goes and never reaches `extent`. `give` 0 is a hard stop.\n */\nexport function rubberband(overshoot: number, extent: number, give: number): number {\n  if (give <= 0 || extent <= 0) return 0;\n  const sign = Math.sign(overshoot);\n  const x = Math.abs(overshoot);\n  return sign * (1 - 1 / ((x * give) / extent + 1)) * extent;\n}\n\n/** `value` held inside [min, max], with the part's give past either end. */\nexport function within(value: number, min: number, max: number, extent: number, give: number): number {\n  if (value < min) return min + rubberband(value - min, extent, give);\n  if (value > max) return max + rubberband(value - max, extent, give);\n  return value;\n}\n\n/** The nearest of `stops` to `value` (a projected landing point picks its step or preset this way). */\nexport function nearest(value: number, stops: readonly number[]): number {\n  let best = stops[0];\n  for (const s of stops) if (Math.abs(s - value) < Math.abs(best - value)) best = s;\n  return best;\n}\n"},{"path":"beamline/lib/id.ts","type":"registry:file","target":"src/ui/beamline/lib/id.ts","content":"let sequence = 0;\n\n/** An id for something made here and now (a sent message, a dropped file, a toast, a new rule): unique within the page\n *  and across reloads, short, and safe in a selector. For an id that must match on the server and the client, useId. */\nexport const localId = (prefix: string) => `${prefix}-${Date.now().toString(36)}${(++sequence).toString(36)}`;\n"},{"path":"beamline/lib/now.ts","type":"registry:file","target":"src/ui/beamline/lib/now.ts","content":"import { useContext, useLayoutEffect, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { ActivityContext } from \"../runtime/activity\";\n\n/*\n * The time, read only in the browser. A component that reads the clock while it renders breaks server rendering:\n * Next.js refuses to prerender a page that does (\"encountered the unstable value `new Date()` in a Client Component\",\n * create-next-app's default since 16), and any server's time differs from the browser's at hydration. These hooks give\n * null on the server and while hydrating, and the real value in the browser before the first paint.\n */\n\nconst still = () => () => {};\nconst startOfToday = () => {\n  const d = new Date();\n  return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();\n};\nconst onServer = () => null;\n\n/** Today at 00:00 local time; null on the server and while hydrating. */\nexport function useToday(): Date | null {\n  const day = useSyncExternalStore(still, startOfToday, onServer);\n  return useMemo(() => (day === null ? null : new Date(day)), [day]);\n}\n\n/** The time in ms, read before the first paint and again every `every` ms while `ticking`; null on the server. */\nexport function useNow(ticking = false, every = 1000, until?: number): number | null {\n  const active = useContext(ActivityContext);\n  const [now, setNow] = useState<number | null>(null);\n  // A finite deadline must reach its completion callback even when the host is idle, then stop ticking.\n  const running = ticking && (until === undefined ? active : now === null || now < until);\n  useLayoutEffect(() => {\n    setNow(Date.now());\n    if (!running) return;\n    const id = setInterval(() => setNow(Date.now()), every);\n    return () => clearInterval(id);\n  }, [running, every, until]);\n  return now;\n}\n"},{"path":"beamline/lib/utils.ts","type":"registry:file","target":"src/ui/beamline/lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n/* The type roles (utilities.css) and the 2xs step are sizes, not text colours: without this, tailwind-merge reads\n   `text-overline` as a colour and drops it next to `text-ink-3`. A role and a size step still replace each other.\n   Likewise `font-title` is the look's title weight, not a family: read as a family it dropped `font-display` beside it,\n   and every merged page title lost the look's display face. */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-weight\": [{ font: [\"title\"] }],\n      \"font-family\": [{ font: [\"display\", \"numeric\", \"figure\"] }],\n      \"font-size\": [\n        {\n          text: [\n            \"2xs\",\n            \"display\",\n            \"title\",\n            \"heading\",\n            \"subheading\",\n            \"body\",\n            \"body-sm\",\n            \"reading\",\n            \"label\",\n            \"caption\",\n            \"overline\",\n            \"numeric\",\n            \"code\",\n          ],\n        },\n      ],\n    },\n  },\n});\n\n/** Merge class names, letting later Tailwind utilities win over earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/** A small seeded random generator (LCG): the same seed gives the same sequence, for stable sample data and jitter. */\nexport function rng(seed = 1) {\n  let s = seed >>> 0 || 1;\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n"}],"docs":"Notification preferences is in src/ui/beamline/blocks/notification-preferences/notification-preferences.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/notification-preferences"}}