{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"settings","type":"registry:block","title":"Settings","description":"Profile and workspace settings with one draft and a real save handler, on grouped panels: photo upload, time zone with the local time, appearance cards, notifications by channel, workspace security, members, plan and usage, API keys and a hold-to-confirm danger zone. Personal mode omits workspace data; optional profile fields share validation, persistence, error recovery and focus return.","categories":["blocks"],"dependencies":["@radix-ui/react-alert-dialog@1.1.23","@radix-ui/react-checkbox@1.3.11","@radix-ui/react-dialog@1.1.23","@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-radio-group@1.4.7","@radix-ui/react-select@2.3.7","@radix-ui/react-slot@1.3.3","@radix-ui/react-switch@1.3.7","@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/settings/messages.ts","type":"registry:file","target":"src/ui/beamline/blocks/settings/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  invitationsSent: {\n    one: \"Invited {count} person\",\n    other: \"Invited {count} people\",\n  },\n  invitationExpiry: \"Their link expires in 7 days.\",\n  roleChanged: \"{name} is now {value}\",\n  roleChangeFailed: \"{name}'s role did not change\",\n  invitationWithdrawn: \"Invitation to {email} withdrawn\",\n  memberRemoved: \"{name} removed\",\n  removeFailed: \"Nobody was removed\",\n  planChanged: \"You're on {value}, billed {period}\",\n  planChangeFailed: \"Your plan did not change\",\n  revoked: \"Revoked {name}\",\n  keyRevokedDescription: \"Requests with it now fail.\",\n  keyRevokeFailed: \"{name} is still active\",\n  deletionScheduledToast: \"Workspace deletion scheduled\",\n  deletionCancellationWindow: \"You have 7 days to cancel.\",\n  navigationLabel: \"Settings sections\",\n  unsaved: \"(unsaved)\",\n  fieldNeedsAttention: \"Field needs attention\",\n  savedAt: \"Saved {value}\",\n  profile: \"Profile\",\n  profileDescription: \"Your name and how to reach you.\",\n  changePhoto: \"Change photo\",\n  name: \"Name\",\n  optional: \"(optional)\",\n  email: \"Email\",\n  emailDescription: \"Used for sign-in and notifications.\",\n  timeZone: \"Time zone\",\n  timeZoneDescription: \"Dates and times show in this zone.\",\n  workspace: \"Workspace\",\n  workspaceDescription: \"Shared by everyone in {workspace}.\",\n  workspaceName: \"Workspace name\",\n  workspaceUrl: \"URL\",\n  workspaceUrlDescription: \"Changing it breaks old links to this workspace.\",\n  members: \"Members\",\n  invite: \"Invite\",\n  inviteTitle: \"Invite teammates\",\n  inviteDescription: \"They get an email with a link that expires in 7 days.\",\n  cancel: \"Cancel\",\n  sendInvites: {\n    \"=0\": \"Send invites\",\n    one: \"Send {count} invite\",\n    other: \"Send {count} invites\",\n  },\n  inviteEmails: \"Emails\",\n  invitePlaceholder: \"name@company.com, then Enter\",\n  role: \"Role\",\n  person: \"Person\",\n  actions: \"Actions\",\n  invited: \"Invited\",\n  memberRoleLabel: \"Role for {name}\",\n  memberActionsLabel: \"Actions for {name}\",\n  withdrawInvitation: \"Withdraw invitation\",\n  removeFromWorkspace: \"Remove from workspace\",\n  withdrawTitle: \"Withdraw the invitation to {email}?\",\n  removeTitle: \"Remove {name}?\",\n  withdrawDescription: \"The link in their email stops working.\",\n  removeDescription: \"They lose access to every deal, report and key in this workspace right away.\",\n  withdraw: \"Withdraw\",\n  remove: \"Remove\",\n  billing: \"Billing\",\n  billingDescription: \"Plan, seats and usage.\",\n  billingPeriod: \"Billing period\",\n  monthly: \"Monthly\",\n  yearly: \"Yearly\",\n  plan: \"Plan\",\n  current: \"Current\",\n  priceUnit: \"/ seat / mo\",\n  planChange: \"Plan change\",\n  keepCurrent: \"Keep current\",\n  switchPlan: \"Switch plan\",\n  seats: \"Seats\",\n  active: \"Active\",\n  notifications: \"Notifications\",\n  notificationsDescription: \"Email, per workspace. Changes wait for Save.\",\n  apiKeys: \"API keys\",\n  apiDescription: \"Keys act as the workspace. After creation only the start of each key is shown.\",\n  createKey: \"Create key\",\n  keyReady: \"{name} is ready\",\n  createKeyTitle: \"Create an API key\",\n  keyReadyDescription: \"Copy it now and store it somewhere safe.\",\n  createKeyDescription: \"It can do anything your workspace can.\",\n  done: \"Done\",\n  copyKey: \"Copy key\",\n  keySecretWarning: \"You won't see this key again. If it is lost, revoke it and create another.\",\n  keyName: \"Key name\",\n  keyNamePlaceholder: \"Production\",\n  keysEmpty: \"No keys yet. Create one for each place that calls the API.\",\n  keyCreated: \"· created {date}\",\n  revoke: \"Revoke\",\n  revokeTitle: \"Revoke {name}?\",\n  revokeDescription: \"Anything still using it stops working at once. This cannot be undone.\",\n  revokeKey: \"Revoke key\",\n  dangerZone: \"Danger zone\",\n  dangerDescription:\n    \"Deleting {workspace} removes every deal, report and key after 7 days. Until then it can be restored by support.\",\n  deletionScheduledDescription: \"{workspace} will be deleted in 7 days. Contact support to cancel.\",\n  deletionScheduled: \"Deletion scheduled\",\n  schedulingDeletion: \"Scheduling deletion…\",\n  deleteAction: \"Hold to delete {workspace}\",\n  unsavedChanges: \"Unsaved changes\",\n  saveResult: \"Save result\",\n  fieldSaveFailed: \"Not saved: 1 field needs attention\",\n  savingElapsed: \"Saving… {elapsed} s\",\n  saving: \"Saving…\",\n  saved: \"Saved\",\n  fixInvalidField: \"· fix the marked field first\",\n  goToField: \"Go to field\",\n  discard: \"Discard\",\n  tryAgain: \"Try again\",\n  save: \"Save\",\n  heading: \"Settings\",\n  roleOwner: \"Owner\",\n  roleAdmin: \"Admin\",\n  roleMember: \"Member\",\n  roleViewer: \"Viewer\",\n  digest: \"Weekly digest\",\n  digestDescription: \"Every Monday at 09:00 in your time zone.\",\n  mentions: \"Mentions and replies\",\n  mentionsDescription: \"When someone @mentions you or replies to your note.\",\n  incidents: \"Incidents\",\n  incidentsDescription: \"Outages and failed webhooks, as they happen.\",\n  product: \"Product updates\",\n  productDescription: \"New features, about once a month.\",\n  nameRequired: \"Enter your name.\",\n  emailInvalid: \"Enter a full email address, like name@company.com.\",\n  workspaceRequired: \"Give the workspace a name.\",\n  slugInvalid: \"Use 3–32 lowercase letters, numbers or dashes.\",\n  fieldRequired: \"Enter {field}.\",\n  saveFailed: \"Settings could not be saved. Try again.\",\n  changesDiscarded: \"Changes discarded.\",\n  inviteFailed: \"The invitations were not sent. Try again.\",\n  retry: \"Try again.\",\n  keyNameRequired: \"Name the key after where it is used: Production, CI.\",\n  keyFailed: \"The key was not created. Try again.\",\n  deleteFailed: \"The workspace was not scheduled for deletion. Try again.\",\n  alreadyMember: \"Already a member.\",\n  emailRequired: \"Not an email address.\",\n  contactSales: \"Talk to us\",\n  savingDiscount: \"Saving {percent}\",\n  planChangeDescription: \"Switch to {plan}, billed {period}. Changes are prorated.\",\n  locale: \"Language and region\",\n  localeDescription: \"The language and formats your application uses after saving.\",\n  localeInvalid: \"Choose a valid language tag, such as uk-UA.\",\n  monthlyPeriod: \"monthly\",\n  yearlyPeriod: \"yearly\",\n  unsavedCount: {\n    one: \"{count} unsaved change\",\n    other: \"{count} unsaved changes\",\n  },\n  membersDescription: {\n    one: \"{count} person · roles decide who can edit billing and API keys.\",\n    other: \"{count} people · roles decide who can edit billing and API keys.\",\n  },\n  membersInvitedDescription: {\n    one: \"{active} active · {count} invited person · roles decide who can edit billing and API keys.\",\n    other: \"{active} active · {count} invited people · roles decide who can edit billing and API keys.\",\n  },\n  groupAccount: \"Account\",\n  identityMembers: {\n    one: \"{count} member\",\n    other: \"{count} members\",\n  },\n  profileAppearanceDescription: \"How you appear to your team, and how the app looks to you.\",\n  photoUpload: \"Upload\",\n  photoChange: \"Change\",\n  photoHint: \"Drop an image or upload. Cropped to a square.\",\n  photoNotImage: \"Choose an image: PNG, JPG or WebP.\",\n  photoUnreadable: \"That image could not be read. Try another file.\",\n  zoneNow: \"{time} there now · {offset}\",\n  appearance: \"Appearance\",\n  appearanceDescription: \"Only on your devices; your team keeps its own.\",\n  appearanceSystem: \"System\",\n  appearanceSystemDetail: \"Follows this device\",\n  appearanceDark: \"Dark\",\n  appearanceDarkDetail: \"Easy on the eyes at night\",\n  appearanceLight: \"Light\",\n  appearanceLightDetail: \"Crisp in a bright room\",\n  security: \"Security\",\n  securityDescription: \"Applies to everyone at their next sign-in.\",\n  requireTwoFactor: \"Require two-step sign-in\",\n  requireTwoFactorDescription: \"Members add an authenticator app or a passkey before they can open anything.\",\n  signInDomains: \"Sign-in domains\",\n  signInDomainsDescription: \"Only addresses at these domains can join. Leave empty to allow any.\",\n  signInDomainsPlaceholder: \"company.com, then Enter\",\n  domainInvalid: \"Not a domain, like company.com.\",\n  notificationsByChannelDescription: \"What reaches you, and where. Changes wait for Save.\",\n  notificationsByChannel: \"Notifications by channel\",\n  notificationByChannel: \"{label} by {channel}\",\n  event: \"Event\",\n  usage: \"Usage\",\n  usageDescription: \"This billing period, updated hourly.\",\n  deleteWorkspaceTitle: \"Delete {workspace}\",\n  deleteWorkspaceDescription: \"Members, deals, reports and keys go with it.\",\n  timeZone2: \"time zone\",\n  workspaceName2: \"workspace name\",\n  workspaceURL: \"workspace URL\",\n  twoStepSignIn: \"two-step sign-in\",\n  signInDomains2: \"sign-in domains\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/blocks/settings/settings.module.css","type":"registry:file","target":"src/ui/beamline/blocks/settings/settings.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  .focusTarget:focus-visible {\n    outline: var(--pui-focus-width) var(--pui-focus-style) var(--pui-focus-ring);\n    outline-offset: 4px;\n    border-radius: var(--pui-radius-control);\n  }\n  .root {\n    container-type: inline-size;\n    /* one column for the form, the tab row and the save bar: 720 wide, the same inline padding everywhere */\n    --_column: 720px;\n    --_gutter: var(--pui-space-4);\n  }\n  @container (min-width: 560px) {\n    .layout {\n      --_gutter: var(--pui-space-8);\n    }\n  }\n  .layout {\n    display: grid;\n    height: 100%;\n    min-height: 0;\n    grid-template-rows: auto minmax(0, 1fr);\n  }\n  .root[data-navigation=\"none\"] .layout {\n    grid-template-rows: minmax(0, 1fr);\n  }\n  /* navigation none: the host's page routes the sections and owns the page, so the form starts at the page's own left\n     edge with no gutter of its own (trial t27: a centred 720 column in a wide page left a gap on the left) */\n  .root[data-navigation=\"none\"] .column {\n    margin-inline: 0;\n    padding-inline: 0;\n    padding-top: 0;\n    /* no room kept for the save bar (t28: a 112 px gap under the form at rest); the bar takes its own when it shows */\n    padding-bottom: var(--pui-space-2);\n  }\n  .root[data-navigation=\"none\"] .saveBar {\n    justify-content: flex-start;\n    margin-block-start: 0;\n    padding-inline: 0;\n  }\n  .root[data-navigation=\"none\"] .bar {\n    max-width: var(--_column);\n  }\n  .column {\n    display: flex;\n    max-width: var(--_column);\n    flex-direction: column;\n    gap: var(--pui-space-6);\n    margin-inline: auto;\n    padding: var(--pui-space-5) var(--_gutter) calc(var(--pui-space-24) + var(--pui-space-4));\n  }\n  @container (min-width: 560px) {\n    .column {\n      padding-top: var(--pui-space-8);\n    }\n  }\n  .navigation {\n    display: flex;\n    min-width: 0;\n    flex-direction: column;\n    border-block-end: 1px solid var(--pui-line-soft);\n    padding: var(--pui-space-3);\n  }\n  .heading {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    gap: var(--pui-space-1) var(--pui-space-3);\n    padding: 0 var(--pui-space-2) var(--pui-space-2);\n  }\n  /* tab row: its heading and items start where the form's text starts (the item's own padding sits in the gutter) */\n  .root[data-navigation=\"tabs\"] .navigation {\n    padding-inline: 0;\n  }\n  .root[data-navigation=\"tabs\"] .heading {\n    width: 100%;\n    max-width: var(--_column);\n    margin-inline: auto;\n    padding: var(--pui-space-2) var(--_gutter) var(--pui-space-3);\n  }\n  .root[data-navigation=\"tabs\"] .items {\n    width: 100%;\n    max-width: var(--_column);\n    margin-inline: auto;\n    padding-inline: calc(var(--_gutter) - var(--pui-space-3));\n  }\n  .items {\n    position: relative;\n    display: flex;\n    gap: var(--pui-space-2);\n    overflow-x: auto;\n    mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);\n  }\n  /* The chosen section wears one plate that travels between the items (useIndicator + the glide class), painted the way\n     Item paints a selection (accent tint, a 2px accent-ink bar at the start), so the items draw none of their own and\n     sit above it. */\n  .items .item {\n    z-index: 1;\n  }\n  .items .item[data-state=\"selected\"],\n  .items .item[data-state=\"selected\"]:hover {\n    background-color: transparent;\n  }\n  .items .item[data-state=\"selected\"]::before {\n    content: none;\n  }\n  .items:dir(rtl) {\n    mask-image: linear-gradient(to left, #000 calc(100% - 24px), transparent);\n  }\n  .saveBar {\n    pointer-events: none;\n    position: sticky;\n    bottom: calc(var(--pui-space-4) + env(safe-area-inset-bottom));\n    z-index: var(--pui-z-raised);\n    display: flex;\n    justify-content: center;\n    /* the bar floats over the column's bottom padding instead of adding its own height under the form */\n    margin-block-start: calc(-1 * var(--pui-space-20));\n    padding-inline: var(--_gutter);\n    padding-block-end: var(--pui-space-4);\n  }\n  .bar {\n    max-width: calc(var(--_column) - 2 * var(--_gutter));\n  }\n  @container (min-width: 760px) {\n    /* beside the rail the sections take the room as grouped panels, not a narrow form in an empty canvas */\n    .root[data-navigation=\"rail\"] .layout {\n      --_column: 1000px;\n    }\n    .root[data-navigation=\"rail\"] .group {\n      display: block;\n    }\n    .root[data-navigation=\"rail\"] .identity {\n      display: flex;\n    }\n    .root[data-navigation=\"rail\"] .layout {\n      grid-template-columns: 220px minmax(0, 1fr);\n      grid-template-rows: minmax(0, 1fr);\n    }\n    .root[data-navigation=\"rail\"] .navigation {\n      border-inline-end: 1px solid var(--pui-line-soft);\n      border-block-end: 0;\n    }\n    .root[data-navigation=\"rail\"] .items {\n      flex-direction: column;\n      mask-image: none;\n    }\n  }\n\n  /* ── the rail: whose settings, then the person's sections and the workspace's ─────────────────────────────── */\n  .identity {\n    display: none;\n    align-items: center;\n    gap: var(--pui-space-3);\n    margin: 0 0 var(--pui-space-3);\n    border-radius: var(--pui-radius-control);\n    padding: var(--pui-space-2) var(--pui-space-3);\n  }\n  .group {\n    display: none;\n    padding: var(--pui-space-4) var(--pui-space-3) var(--pui-space-1);\n  }\n  .group:first-child {\n    padding-top: var(--pui-space-1);\n  }\n\n  /* ── a section's groups ──────────────────────────────────────────────────────────────────────────────── */\n  .panel {\n    display: flex;\n    min-width: 0;\n    flex-direction: column;\n    gap: var(--pui-space-5);\n    padding: var(--pui-space-5);\n  }\n  @container (min-width: 560px) {\n    .panel:not([data-flush]) {\n      padding: var(--pui-space-6);\n    }\n  }\n  .panel[data-flush] {\n    gap: 0;\n    overflow: hidden;\n    padding: 0;\n  }\n  .panel[data-flat] {\n    padding: 0;\n  }\n  .panelHeader {\n    display: grid;\n    gap: var(--pui-space-0-5);\n  }\n  .panel[data-flush] > .panelHeader {\n    border-block-end: 1px solid var(--pui-line-soft);\n    padding: var(--pui-space-4) var(--pui-space-5);\n  }\n  .panel[data-tone=\"danger\"] {\n    box-shadow:\n      var(--m-surface-edge),\n      inset 0 0 0 1px color-mix(in oklch, var(--pui-red) 45%, transparent);\n  }\n  .fields {\n    display: grid;\n    align-content: start;\n    gap: var(--pui-space-4) var(--pui-space-5);\n  }\n  @container (min-width: 520px) {\n    .fields {\n      grid-template-columns: repeat(2, minmax(0, 1fr));\n    }\n  }\n  .identityGrid {\n    display: grid;\n    gap: var(--pui-space-5);\n  }\n  @container (min-width: 600px) {\n    .identityGrid {\n      grid-template-columns: 176px minmax(0, 1fr);\n      gap: var(--pui-space-8);\n    }\n  }\n  /* the photo sits in a well that takes a dropped image */\n  .photo {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    gap: var(--pui-space-3);\n    border-radius: var(--pui-radius-card);\n    padding: var(--pui-space-4) var(--pui-space-3);\n    outline: 1px dashed transparent;\n    outline-offset: -1px;\n    transition:\n      outline-color var(--pui-motion-out),\n      background-color var(--pui-motion-out);\n  }\n  .photo[data-state=\"dropping\"] {\n    outline-color: var(--pui-accent);\n    background-color: var(--pui-accent-tint);\n  }\n  .photo .photoAvatar {\n    --_s: 88px;\n    --_d: 26px;\n  }\n  .photo > p {\n    text-align: center;\n  }\n  /* a narrow panel puts the photo beside its buttons instead of over them */\n  @container (max-width: 599px) {\n    .photo {\n      display: grid;\n      grid-template-columns: auto minmax(0, 1fr);\n      align-items: center;\n      justify-items: start;\n      gap: var(--pui-space-1) var(--pui-space-4);\n      padding: var(--pui-space-3);\n    }\n    .photo .photoAvatar {\n      --_s: 64px;\n      --_d: 20px;\n      grid-row: span 2;\n    }\n    .photo > p {\n      text-align: start;\n    }\n  }\n  .root[data-navigation=\"none\"] .photo {\n    align-items: center;\n    justify-content: flex-start;\n    flex-direction: row;\n    padding: 0;\n  }\n\n  /* ── appearance: each card shows a small window in its scheme, drawn from the theme's own colours ─────── */\n  .appearance [data-slot=\"radio-cards-media\"] {\n    aspect-ratio: 16 / 7;\n  }\n  .preview {\n    --_bg: oklch(from var(--pui-canvas) 0.985 0.004 h);\n    --_rail: oklch(from var(--pui-canvas) 0.94 0.006 h);\n    --_ink: oklch(from var(--pui-ink) 0.3 c h / 0.22);\n    display: flex;\n    width: 100%;\n    height: 100%;\n    overflow: hidden;\n    border-radius: var(--pui-radius-control);\n    background: var(--_bg);\n    box-shadow: inset 0 0 0 1px var(--pui-line-soft);\n  }\n  .preview[data-scheme=\"dark\"] {\n    --_bg: oklch(from var(--pui-canvas) 0.19 0.008 h);\n    --_rail: oklch(from var(--pui-canvas) 0.235 0.008 h);\n    --_ink: oklch(from var(--pui-ink) 0.92 c h / 0.22);\n  }\n  .preview[data-scheme=\"system\"] {\n    background: linear-gradient(\n      115deg,\n      oklch(from var(--pui-canvas) 0.985 0.004 h) 0 50%,\n      oklch(from var(--pui-canvas) 0.19 0.008 h) 50% 100%\n    );\n    --_rail: oklch(from var(--pui-canvas) 0.6 0.006 h / 0.25);\n    --_ink: oklch(from var(--pui-ink) 0.6 c h / 0.35);\n  }\n  .previewRail {\n    width: 26%;\n    background: var(--_rail);\n  }\n  .previewBody {\n    display: flex;\n    flex: 1;\n    flex-direction: column;\n    gap: 6px;\n    padding: 12% 10%;\n  }\n  .previewLine {\n    height: 5px;\n    border-radius: var(--pui-radius-pill);\n    background: var(--_ink);\n  }\n  .previewLine:nth-child(2) {\n    width: 64%;\n  }\n  .previewAccent {\n    width: 38%;\n    height: 9px;\n    margin-top: auto;\n    border-radius: var(--pui-radius-pill);\n    /* the brand, kept readable on the scheme it sits on */\n    background: oklch(from var(--pui-accent) min(l, 0.55) max(c, 0.02) h);\n  }\n  .preview[data-scheme=\"dark\"] .previewAccent {\n    background: oklch(from var(--pui-accent) max(l, 0.78) c h);\n  }\n\n  /* ── rows inside a panel: notifications, keys ─────────────────────────────────────────────────────────── */\n  .row {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: var(--pui-space-6);\n    border-block-end: 1px solid var(--pui-line-soft);\n    padding-block: var(--pui-space-4);\n  }\n  .row:last-child {\n    border-block-end: 0;\n  }\n  .panel:not([data-flush]) {\n    > .row:first-child {\n      padding-block-start: 0;\n    }\n    > .row:last-child {\n      padding-block-end: 0;\n    }\n  }\n  .panel:not([data-flush]) > .row {\n    padding-block: var(--pui-space-4);\n  }\n  .panel:not([data-flush]) > .row + .row {\n    border-block-start: 0;\n  }\n  .panel:not([data-flush]):has(> .row) {\n    gap: 0;\n  }\n  .panel[data-flush] > .row {\n    padding-inline: var(--pui-space-5);\n  }\n  .keyRow {\n    justify-content: flex-start;\n    gap: var(--pui-space-3);\n  }\n  .keyIcon {\n    display: grid;\n    width: 32px;\n    height: 32px;\n    flex-shrink: 0;\n    place-items: center;\n    border-radius: var(--pui-radius-control);\n    color: var(--pui-ink-3);\n  }\n  .meters {\n    display: grid;\n    gap: var(--pui-space-6);\n  }\n  @container (min-width: 560px) {\n    .meters {\n      grid-template-columns: repeat(2, minmax(0, 1fr));\n      gap: var(--pui-space-8);\n    }\n  }\n  .dangerRow {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    justify-content: space-between;\n    gap: var(--pui-space-3) var(--pui-space-6);\n  }\n\n  /* ── notifications by channel ─────────────────────────────────────────────────────────────────────────── */\n  .matrix {\n    width: 100%;\n    border-collapse: separate;\n    border-spacing: 0;\n  }\n  .matrix th,\n  .matrix td {\n    border-block-end: 1px solid var(--pui-line-soft);\n    padding: var(--pui-space-3) var(--pui-space-4);\n    font-weight: inherit;\n  }\n  /* cells by name, never `> *`: a last-child test followed by \"any\" makes every insert beside the page root (a portal,\n     a chart's colour probe) restyle the whole page */\n  .matrix tbody tr:last-child > :is(th, td) {\n    border-block-end: 0;\n  }\n  .matrix th[scope=\"row\"] {\n    padding-inline-start: var(--pui-space-5);\n    text-align: start;\n  }\n  .matrix thead th {\n    padding-block: var(--pui-space-3);\n    color: var(--pui-ink-2);\n    font-size: var(--pui-text-xs);\n  }\n  .matrix td {\n    width: 96px;\n    text-align: center;\n  }\n  .matrix tbody tr {\n    transition: background-color var(--pui-motion-out);\n  }\n  .matrix tbody tr:hover {\n    background-color: var(--pui-hover);\n  }\n  .channel {\n    display: inline-flex;\n    align-items: center;\n    gap: var(--pui-space-1-5);\n  }\n  .channel > svg {\n    width: 14px;\n    height: 14px;\n    color: var(--pui-ink-3);\n  }\n\n  /* ── the save bar wears its state: a mark at the start and a line along the bottom that fills while it saves,\n        then turns green when saved or red when it failed ─────────────────────────────────────────────────── */\n  .bar {\n    position: relative;\n    overflow: hidden;\n  }\n  .saveLine {\n    position: absolute;\n    inset-inline: 0;\n    bottom: 0;\n    height: 2px;\n    background: var(--pui-accent);\n    opacity: 0;\n    transform: scaleX(0);\n    transform-origin: left;\n    transition:\n      transform var(--pui-motion-out),\n      opacity var(--pui-motion-out),\n      background-color var(--pui-motion-out);\n  }\n  .saveLine:dir(rtl) {\n    transform-origin: right;\n  }\n  .bar[data-state=\"saving\"] .saveLine {\n    opacity: 1;\n    transform: scaleX(0.82);\n    /* a long ease-out: it keeps moving for as long as a save usually takes, then waits near the end */\n    transition-duration: calc(var(--pui-duration-sweep) * 2), var(--pui-duration-fast);\n    transition-timing-function: var(--pui-ease-exit);\n  }\n  .bar[data-state=\"saved\"] .saveLine,\n  .bar[data-state=\"failed\"] .saveLine {\n    opacity: 1;\n    transform: scaleX(1);\n  }\n  .bar[data-state=\"saved\"] .saveLine {\n    background: var(--pui-green);\n  }\n  .bar[data-state=\"failed\"] .saveLine {\n    background: var(--pui-red);\n  }\n  .saveIcon {\n    display: grid;\n    width: 22px;\n    height: 22px;\n    flex-shrink: 0;\n    place-items: center;\n    border-radius: var(--pui-radius-pill);\n    background: var(--pui-accent-tint);\n    color: var(--pui-accent-ink);\n  }\n  .saveIcon:empty::before {\n    content: \"\";\n    width: 7px;\n    height: 7px;\n    border-radius: var(--pui-radius-pill);\n    background: var(--pui-accent);\n  }\n  .bar[data-state=\"saving\"] .saveIcon:empty::before {\n    transform: scale(0.6);\n    transition: transform var(--pui-motion-out);\n  }\n  .bar[data-state=\"saved\"] .saveIcon {\n    background: var(--pui-green-tint);\n    color: var(--pui-green);\n  }\n  .bar[data-state=\"failed\"] .saveIcon {\n    background: var(--pui-red-tint);\n    color: var(--pui-red);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.identity {\n  composes: m-inset from global;\n}\n.keyIcon {\n  composes: m-inset from global;\n}\n.items {\n  composes: scrollbar-none from global;\n}\n.plate {\n  composes: plate-current from global;\n}\n"},{"path":"beamline/blocks/settings/settings.tsx","type":"registry:file","target":"src/ui/beamline/blocks/settings/settings.tsx","content":"\"use client\";\n\nimport { type MessageLabels, type Translator, dateFormatter, useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { createContext, useContext, useEffect, useId, useRef, useState, useLayoutEffect } from \"react\";\nimport type { ReactNode, Ref, CSSProperties } from \"react\";\nimport {\n  Bell,\n  Building2,\n  Check,\n  Clock,\n  CreditCard,\n  ImageUp,\n  KeyRound,\n  Mail,\n  MessageSquare,\n  MoreHorizontal,\n  Plus,\n  ShieldAlert,\n  Smartphone,\n  TriangleAlert,\n  User,\n  Users,\n} from \"lucide-react\";\nimport { Alert } from \"../../components/alert/alert\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogFooter,\n} from \"../../components/alert-dialog/alert-dialog\";\nimport { Avatar } from \"../../components/avatar/avatar\";\nimport { Badge } from \"../../components/badge/badge\";\nimport { Button } from \"../../components/button/button\";\nimport { Combobox } from \"../../components/combobox/combobox\";\nimport { CopyButton } from \"../../components/copy-button/copy-button\";\nimport { Dialog, DialogContent, DialogTrigger } from \"../../components/dialog/dialog\";\nimport { DropdownMenu } from \"../../components/dropdown-menu/dropdown-menu\";\nimport { HoldToConfirm } from \"../../components/hold-to-confirm/hold-to-confirm\";\nimport { Input } from \"../../components/input/input\";\nimport { Item } from \"../../components/item/item\";\nimport { RadioCards } from \"../../components/radio-cards/radio-cards\";\nimport { SegmentedControl } from \"../../components/segmented-control/segmented-control\";\nimport { Select } from \"../../components/select/select\";\nimport { SlotText } from \"../../components/slot-text/slot-text\";\nimport { Switch } from \"../../components/switch/switch\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../components/table/table\";\nimport { TagInput } from \"../../components/tag-input/tag-input\";\nimport { TextMorph } from \"../../components/text-morph/text-morph\";\nimport { ToastStack, ToastStackProvider, useToastStack } from \"../../components/toast/toast\";\nimport { UsageMeter, type UsageMeterSegment } from \"../../components/usage-meter/usage-meter\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { glideProps, useIndicator } from \"../../hooks/use-indicator\";\nimport { cn } from \"../../lib/utils\";\nimport { useExpiring } from \"../../hooks/use-expiring\";\nimport { useElapsed } from \"../../hooks/use-elapsed\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport styles from \"./settings.module.css\";\nimport { useNow } from \"../../lib/now\";\n\nexport type SettingsSection = \"profile\" | \"workspace\" | \"members\" | \"billing\" | \"notifications\" | \"api\" | \"danger\";\nexport type BillingPeriod = \"monthly\" | \"yearly\";\n\n/** Everything the save bar saves: profile, workspace and email notifications. */\nexport interface SettingsValues {\n  /** Human-readable name of this record. Required. */\n  name: string;\n  /** Email address shown for this person. Required. */\n  email: string;\n  /** IANA time zone for this person. Required. */\n  timeZone: string;\n  /** Optional BCP 47 language tag; not restricted to the suggestion list. */\n  locale?: string;\n  /** Workspace name. Omitted by default. */\n  workspace?: string;\n  /** Workspace URL slug. Omitted by default. */\n  slug?: string;\n  /** The person's photo as a URL or data URL; \"\" for none. Present (even empty) to offer upload in Profile: a chosen\n   *  image is cropped to a square, kept in the draft and saved with everything else. */\n  avatar?: string;\n  /** The chosen appearance (an appearanceOptions value). Present to show the Appearance cards in Profile. */\n  appearance?: string;\n  /** Workspace security, shown in Workspace when present: everyone signs in with a second step. */\n  requireTwoFactor?: boolean;\n  /** Workspace security, shown in Workspace when present: only these email domains may join. */\n  allowedDomains?: string[];\n  /** Additional profile values, keyed by profileFields. */\n  profile?: Record<string, string>;\n  /** On or off per notification id; with notificationChannels, per `${notification}.${channel}`. */\n  notifications: Record<string, boolean>;\n}\n/** An appearance card: its preview is drawn for the built-in system, dark and light, or pass your own. */\nexport interface SettingsAppearance {\n  value: string;\n  label: string;\n  detail?: string;\n  preview?: ReactNode;\n}\n/** A notification channel: a column of switches in the notification matrix. */\nexport interface SettingsChannel {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n}\nexport interface SettingsProfileField {\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  /** Whether an empty value is permitted. Omitted by default. */\n  optional?: boolean;\n  /** Supporting content describing this item. Omitted by default. */\n  description?: string;\n  /** Native autocomplete hint for this field. Omitted by default. */\n  autoComplete?: string;\n  /** Maximum number of characters accepted by the field. Omitted by default. */\n  maxLength?: number;\n  /** Returns a validation error for this field, or undefined to accept its value. Omitted by default. */\n  validate?: (value: string) => string | undefined;\n}\n\nexport interface SettingsMember {\n  /** Stable identifier used to match this record across updates. Required. */\n  id: string;\n  /** Human-readable name of this record. Required. */\n  name: string;\n  /** Email address shown for this person. Required. */\n  email: string;\n  /** Role assigned to this person. Required. */\n  role: string;\n  /** URL of the person avatar. Omitted by default. */\n  avatar?: string;\n  /** Whether this member is awaiting invitation acceptance. Omitted by default. */\n  pending?: boolean;\n}\nexport interface SettingsRole {\n  /** Value represented by this item. Required. */\n  value: string;\n  /** Text displayed for this item. Required. */\n  label: string;\n}\n/** A plan's price per seat per month for each period; null when it is quoted by sales. */\nexport interface SettingsPlan {\n  /** Stable identifier used to match this record across updates. Required. */\n  id: string;\n  /** Human-readable name of this record. Required. */\n  name: string;\n  /** Additional content describing this item. Omitted by default. */\n  detail?: string;\n  /** Monthly and yearly prices, or null when pricing is unavailable. Required. */\n  price: { monthly: number; yearly: number } | null;\n}\nexport interface SettingsUsage {\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  /** Unit printed beside the amount. Omitted by default. */\n  unit?: string;\n  /** Maximum allowed amount for this allowance. Required. */\n  limit: number;\n  /** Fraction of the limit at which the usage warning starts. Omitted by default. */\n  warnAt?: number;\n  /** Parts that make up the measured usage. Required. */\n  segments: UsageMeterSegment[];\n}\n/** A key as listed after creation: only its start is ever shown again. */\nexport interface SettingsApiKey {\n  /** Stable identifier used to match this record across updates. Required. */\n  id: string;\n  /** Human-readable name of this record. Required. */\n  name: string;\n  /** Non-secret API key prefix shown for identification. Required. */\n  prefix: string;\n  /** Creation timestamp in milliseconds since the Unix epoch. Required. */\n  created: number;\n}\nexport interface SettingsNotification {\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  /** Supporting content describing this item. Required. */\n  description: string;\n}\n\nexport type SettingsClassNames = Partial<\n  Record<\n    | \"button\"\n    | \"avatar\"\n    | \"name\"\n    | \"email\"\n    | \"profileField\"\n    | \"workspace\"\n    | \"slug\"\n    | \"timeZone\"\n    | \"inviteEmails\"\n    | \"role\"\n    | \"dialog\"\n    | \"table\"\n    | \"tableHeader\"\n    | \"tableBody\"\n    | \"tableRow\"\n    | \"confirmation\"\n    | \"confirmationFooter\"\n    | \"confirmationCancel\"\n    | \"confirmationAction\"\n    | \"memberActions\"\n    | \"badge\"\n    | \"period\"\n    | \"plans\"\n    | \"usage\"\n    | \"notificationSwitch\"\n    | \"copyKey\"\n    | \"alert\"\n    | \"keyName\"\n    | \"delete\"\n    | \"toasts\"\n    | \"root\"\n    | \"layout\"\n    | \"navigation\"\n    | \"heading\"\n    | \"title\"\n    | \"items\"\n    | \"item\"\n    | \"unsavedLabel\"\n    | \"plate\"\n    | \"fieldAttention\"\n    | \"dirty\"\n    | \"viewport\"\n    | \"body\"\n    | \"savedAt\"\n    | \"column\"\n    | \"photoRow\"\n    | \"optional\"\n    | \"domain\"\n    | \"inviteIcon\"\n    | \"inviteForm\"\n    | \"inviteError\"\n    | \"roleHeader\"\n    | \"actionsHeader\"\n    | \"actionsLabel\"\n    | \"personCell\"\n    | \"person\"\n    | \"personBody\"\n    | \"personName\"\n    | \"personEmail\"\n    | \"roleLabel\"\n    | \"memberActionsIcon\"\n    | \"planName\"\n    | \"planChange\"\n    | \"planDescription\"\n    | \"wantedPlan\"\n    | \"notification\"\n    | \"notificationBody\"\n    | \"notificationLabel\"\n    | \"notificationDescription\"\n    | \"keyCreateIcon\"\n    | \"secretBody\"\n    | \"secretRow\"\n    | \"secret\"\n    | \"keyForm\"\n    | \"keysEmpty\"\n    | \"key\"\n    | \"keyIcon\"\n    | \"keyBody\"\n    | \"keyTitle\"\n    | \"keyMeta\"\n    | \"keyPrefix\"\n    | \"keyCreated\"\n    | \"danger\"\n    | \"deleteError\"\n    | \"announcement\"\n    | \"saveRegion\"\n    | \"saveBar\"\n    | \"saveDescription\"\n    | \"saveError\"\n    | \"saved\"\n    | \"invalid\"\n    | \"section\"\n    | \"sectionHeader\"\n    | \"sectionIdentity\"\n    | \"sectionHeading\"\n    | \"sectionDescription\"\n    | \"identity\"\n    | \"identityName\"\n    | \"identityMeta\"\n    | \"group\"\n    | \"panel\"\n    | \"panelHeader\"\n    | \"panelTitle\"\n    | \"panelDescription\"\n    | \"photo\"\n    | \"photoActions\"\n    | \"photoHint\"\n    | \"photoError\"\n    | \"zone\"\n    | \"appearance\"\n    | \"preview\"\n    | \"matrix\"\n    | \"security\"\n    | \"saveIcon\",\n    string\n  >\n>;\nexport interface SettingsProps {\n  /** Per-instance copy overrides UIRoot messages. */\n  labels?: MessageLabels<typeof englishMessages>;\n  /** Suggested language tags; any valid BCP 47 saved value remains supported. */\n  localeOptions?: SettingsLocaleOption[];\n  /** Currency for built-in plan prices. */\n  currency?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SettingsClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Use tabs inside an app with its own navigation; none when the host routes sections: then the block sits flush in\n   *  the host's page (its background, its left edge, its heading) and shows only the section's form. */\n  navigation?: \"rail\" | \"tabs\" | \"none\";\n  /** The heading over the section list (rail, tabs). Null leaves it to the host; with navigation none the host's page\n   *  has the heading and this is not shown. */\n  heading?: ReactNode | null;\n  /** Beside the heading, outside it: a status the page owns, such as where saves are kept (\"Saves stay in this\n   *  browser\"). Shown only with a heading. */\n  headingAside?: ReactNode;\n  /** Extra profile Inputs use the same draft, validation and save handler. */\n  profileFields?: SettingsProfileField[];\n  /** The section in view, controlled or not. */\n  section?: SettingsSection;\n  /** Initial visible settings section when uncontrolled. Defaults to \"profile\". */\n  defaultSection?: SettingsSection;\n  /** Reports visible settings section after a change; no callback by default. */\n  onSectionChange?: (section: SettingsSection) => void;\n  /** The saved values, controlled (update them from onValuesChange) or not. */\n  values?: SettingsValues;\n  /** Initial field values when uncontrolled. Required. */\n  defaultValues: SettingsValues;\n  /** Controlled pending edits over saved values; omitted by default to keep the draft locally. */\n  draft?: Partial<SettingsValues>;\n  /** Restored pending edits applied once when uncontrolled; defaults to no pending edits. */\n  defaultDraft?: Partial<SettingsValues>;\n  /** Reports pending edits on typing, discard or successful save; no callback by default. */\n  onDraftChange?: (draft: Partial<SettingsValues>) => void;\n  /** Reports field values after a change; no callback by default. */\n  onValuesChange?: (values: SettingsValues) => void;\n  /** Persist the draft. Reject to keep the save bar with your message. */\n  onSave: (values: SettingsValues, changed: string[]) => Promise<void>;\n  /** Shows \"Change photo\" next to the avatar. */\n  onPhotoChange?: () => void;\n  /** IANA zones for the time zone field; an empty list leaves the field out. */\n  timeZones?: string[];\n  /** The address the workspace URL sits under: \"northline.example\". */\n  domain?: string;\n  /** Email notifications with their switches. */\n  notificationOptions?: SettingsNotification[];\n  /** Channels (Email, Push, Slack): each notification gets a switch per channel, as a matrix. */\n  notificationChannels?: SettingsChannel[];\n  /** The Appearance cards, when values carry an appearance. */\n  appearanceOptions?: SettingsAppearance[];\n  /** Members, controlled or not; the Members section appears with them. */\n  members?: SettingsMember[];\n  /** Initial member records; omitted by default to hide the members section unless members is supplied. */\n  defaultMembers?: SettingsMember[];\n  /** Reports member records after a change; no callback by default. */\n  onMembersChange?: (members: SettingsMember[]) => void;\n  /** Roles that can be given; \"owner\" is shown but never offered. */\n  roles?: SettingsRole[];\n  /** Send invitations; resolve with the new (pending) members. */\n  onInvite?: (emails: string[], role: string) => Promise<SettingsMember[]>;\n  /** Reports role after a change; no callback by default. */\n  onRoleChange?: (memberId: string, role: string) => Promise<void>;\n  /** Removes the specified workspace member; no handler by default. */\n  onRemoveMember?: (memberId: string) => Promise<void>;\n  /** Plans; the Billing section appears with them. */\n  plans?: SettingsPlan[];\n  /** Controlled subscribed plan id; omit to use defaultPlan. */\n  plan?: string;\n  /** Initial subscribed plan id; defaults to the first offered plan, then an empty string. */\n  defaultPlan?: string;\n  /** Controlled billing period; omit to use defaultPeriod. */\n  period?: BillingPeriod;\n  /** Initial billing period when uncontrolled; defaults to yearly. */\n  defaultPeriod?: BillingPeriod;\n  /** Reports the billing period after onPlanChange succeeds; no callback by default. */\n  onPeriodChange?: (period: BillingPeriod) => void;\n  /** Change plan or billing period; resolve once billing has it. */\n  onPlanChange?: (plan: string, period: BillingPeriod) => Promise<void>;\n  /** Seats the plan includes (the seats meter counts members). */\n  seats?: number;\n  /** Other metered allowances. */\n  usage?: SettingsUsage[];\n  /** API keys, controlled or not; the API keys section appears with them. */\n  apiKeys?: SettingsApiKey[];\n  /** Initial API key records; omitted by default to hide that section unless apiKeys is supplied. */\n  defaultApiKeys?: SettingsApiKey[];\n  /** Reports API keys after a change; no callback by default. */\n  onApiKeysChange?: (keys: SettingsApiKey[]) => void;\n  /** Create a key; resolve with it and its secret, which is shown once. */\n  onCreateKey?: (name: string) => Promise<{ key: SettingsApiKey; secret: string }>;\n  /** Revokes the specified API key; no handler by default. */\n  onRevokeKey?: (keyId: string) => Promise<void>;\n  /** Schedule the workspace for deletion; the Danger zone appears with it. */\n  onDeleteWorkspace?: () => Promise<void>;\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}\n\nexport interface SettingsLocaleOption {\n  value: string;\n  label?: string;\n}\nexport const SETTINGS_LOCALE_OPTIONS: SettingsLocaleOption[] = [\n  \"en-US\",\n  \"en-GB\",\n  \"de-DE\",\n  \"fr-FR\",\n  \"es-ES\",\n  \"ja-JP\",\n].map((value) => ({ value }));\nconst ROLES = (i18n: Translator<typeof englishMessages>): SettingsRole[] => [\n  { value: \"owner\", label: i18n(\"roleOwner\") },\n  { value: \"admin\", label: i18n(\"roleAdmin\") },\n  { value: \"member\", label: i18n(\"roleMember\") },\n  { value: \"viewer\", label: i18n(\"roleViewer\") },\n];\nconst ZONES = [\n  \"Europe/Berlin\",\n  \"Europe/London\",\n  \"America/New_York\",\n  \"America/Los_Angeles\",\n  \"Asia/Singapore\",\n  \"Asia/Tokyo\",\n  \"Australia/Sydney\",\n  \"UTC\",\n];\nconst APPEARANCES = (i18n: Translator<typeof englishMessages>): SettingsAppearance[] => [\n  { value: \"system\", label: i18n(\"appearanceSystem\"), detail: i18n(\"appearanceSystemDetail\") },\n  { value: \"dark\", label: i18n(\"appearanceDark\"), detail: i18n(\"appearanceDarkDetail\") },\n  { value: \"light\", label: i18n(\"appearanceLight\"), detail: i18n(\"appearanceLightDetail\") },\n];\n/** The channel icons a matrix shows when a channel brings none. */\nexport const SETTINGS_CHANNEL_ICONS: Record<string, ReactNode> = {\n  email: <Mail />,\n  push: <Smartphone />,\n  slack: <MessageSquare />,\n};\nconst NOTIFICATIONS = (i18n: Translator<typeof englishMessages>): SettingsNotification[] => [\n  { id: \"digest\", label: i18n(\"digest\"), description: i18n(\"digestDescription\") },\n  { id: \"mentions\", label: i18n(\"mentions\"), description: i18n(\"mentionsDescription\") },\n  { id: \"incidents\", label: i18n(\"incidents\"), description: i18n(\"incidentsDescription\") },\n  { id: \"product\", label: i18n(\"product\"), description: i18n(\"productDescription\") },\n];\nconst ICONS: Record<SettingsSection, ReactNode> = {\n  profile: <User className=\"size-4\" />,\n  workspace: <Building2 className=\"size-4\" />,\n  members: <Users className=\"size-4\" />,\n  billing: <CreditCard className=\"size-4\" />,\n  notifications: <Bell className=\"size-4\" />,\n  api: <KeyRound className=\"size-4\" />,\n  danger: <ShieldAlert className=\"size-4\" />,\n};\nconst LABELS = (i18n: Translator<typeof englishMessages>): Record<SettingsSection, string> => ({\n  profile: i18n(\"profile\"),\n  workspace: i18n(\"workspace\"),\n  members: i18n(\"members\"),\n  billing: i18n(\"billing\"),\n  notifications: i18n(\"notifications\"),\n  api: i18n(\"apiKeys\"),\n  danger: i18n(\"dangerZone\"),\n});\nfunction localizedFIELD_NAMES(i18n: Translator<typeof englishMessages>): Record<string, string> {\n  return {\n    locale: \"locale\",\n    name: \"name\",\n    email: \"email\",\n    timeZone: i18n(\"timeZone2\"),\n    workspace: i18n(\"workspaceName2\"),\n    slug: i18n(\"workspaceURL\"),\n    avatar: \"photo\",\n    appearance: \"appearance\",\n    requireTwoFactor: i18n(\"twoStepSignIn\"),\n    allowedDomains: i18n(\"signInDomains2\"),\n  };\n}\n/** The workspace fields; every other draft field belongs to Profile (or Notifications). */\nconst WORKSPACE_FIELDS = [\"workspace\", \"slug\", \"requireTwoFactor\", \"allowedDomains\"];\nconst same = (a: unknown, b: unknown) =>\n  Array.isArray(a) && Array.isArray(b) ? a.length === b.length && a.every((v, i) => v === b[i]) : a === b;\n/** What it is in a time zone now, in the reader's locale, and its offset: \"14:05\", \"GMT+2\". */\nfunction zoneNow(zone: string, now: number | null, locale?: string) {\n  if (now === null) return null;\n  try {\n    const time = dateFormatter(locale, { timeZone: zone, hour: \"2-digit\", minute: \"2-digit\" }).format(now);\n    const offset =\n      dateFormatter(\"en-US\", { timeZone: zone, timeZoneName: \"shortOffset\" })\n        .formatToParts(now)\n        .find((p) => p.type === \"timeZoneName\")?.value ?? \"\";\n    return { time, offset: offset.replace(/^GMT$/, \"GMT+0\") };\n  } catch {\n    return null;\n  }\n}\n/** A chosen image, cropped to its centre square and scaled to 192 px, as a data URL small enough to keep. */\nasync function squarePhoto(file: File) {\n  const bitmap = await createImageBitmap(file);\n  const side = Math.min(bitmap.width, bitmap.height);\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = canvas.height = 192;\n  canvas\n    .getContext(\"2d\")\n    ?.drawImage(bitmap, (bitmap.width - side) / 2, (bitmap.height - side) / 2, side, side, 0, 0, 192, 192);\n  bitmap.close();\n  return canvas.toDataURL(\"image/webp\", 0.86);\n}\nconst validLocale = (value: string) => {\n  try {\n    return Intl.getCanonicalLocales(value).length === 1;\n  } catch {\n    return false;\n  }\n};\nconst isEmail = (v: string) => /^[^\\s@]+@[^\\s@.]+(\\.[^\\s@.]+)+$/.test(v.trim());\nconst message = (e: unknown, fallback: string) => (e instanceof Error && e.message ? e.message : fallback);\n\n/**\n * Workspace settings: a section list and one section at a time. Profile, workspace and notification edits collect in\n * a draft and a save bar offers Save or Discard; your onSave's promise drives it. Invites, role changes, plan changes,\n * keys and deletion each call their own handler and confirm with a toast; destructive ones ask first.\n */\nexport function Settings(props: SettingsProps) {\n  return (\n    <ToastStackProvider>\n      <SettingsBody {...props} />\n    </ToastStackProvider>\n  );\n}\n\nfunction SettingsBody({\n  labels,\n  localeOptions: localeOptionsProp,\n  currency = \"USD\",\n  navigation = \"rail\",\n  heading: headingProp,\n  headingAside,\n  profileFields = [],\n  section: sectionProp,\n  defaultSection = \"profile\",\n  onSectionChange,\n  values,\n  defaultValues,\n  draft: draftProp,\n  defaultDraft = {},\n  onDraftChange,\n  onValuesChange,\n  onSave,\n  onPhotoChange,\n  timeZones = ZONES,\n  domain = \"app.example.com\",\n  notificationOptions: notificationOptionsProp,\n  notificationChannels,\n  appearanceOptions: appearanceOptionsProp,\n  members: membersProp,\n  defaultMembers,\n  onMembersChange,\n  roles: rolesProp,\n  onInvite,\n  onRoleChange,\n  onRemoveMember,\n  plans,\n  plan: planProp,\n  defaultPlan,\n  period: periodProp,\n  defaultPeriod = \"yearly\",\n  onPeriodChange,\n  onPlanChange,\n  seats,\n  usage = [],\n  apiKeys: keysProp,\n  defaultApiKeys,\n  onApiKeysChange,\n  onCreateKey,\n  onRevokeKey,\n  onDeleteWorkspace,\n  className,\n  classNames,\n  style,\n  ref,\n}: SettingsProps) {\n  const i18n = useMessages(\"settings\", englishMessages, labels);\n  const localeOptions = localeOptionsProp ?? SETTINGS_LOCALE_OPTIONS;\n  const heading = headingProp === undefined ? i18n(\"heading\") : headingProp;\n  const notificationOptions = notificationOptionsProp ?? NOTIFICATIONS(i18n);\n  const appearanceOptions = appearanceOptionsProp ?? APPEARANCES(i18n);\n  const roles = rolesProp ?? ROLES(i18n);\n  const sectionLabels = LABELS(i18n);\n  const day = (value: number) => i18n.date(value, { month: \"short\", day: \"numeric\", year: \"numeric\" });\n\n  const { toast } = useToastStack();\n  const root = useRef<HTMLDivElement | null>(null);\n  const localeField = useRef<HTMLButtonElement>(null);\n  const composedRef = useComposedRef(root, ref);\n  const pendingFocus = useRef<string | null>(null);\n  const hasWorkspace =\n    (values ?? defaultValues).workspace !== undefined || (values ?? defaultValues).slug !== undefined;\n  const hasMembers = !!(membersProp ?? defaultMembers);\n  const hasKeys = !!(keysProp ?? defaultApiKeys);\n  const sections: SettingsSection[] = [\n    \"profile\",\n    \"notifications\",\n    ...(hasWorkspace ? [\"workspace\" as const] : []),\n    ...(hasMembers ? [\"members\" as const] : []),\n    ...(plans?.length ? [\"billing\" as const] : []),\n    ...(hasKeys ? [\"api\" as const] : []),\n    ...(onDeleteWorkspace ? [\"danger\" as const] : []),\n  ];\n  const [sectionRaw, setSection] = useControllable(sectionProp, defaultSection, onSectionChange);\n  const section = sections.includes(sectionRaw) ? sectionRaw : \"profile\";\n  // the chosen section's plate travels between items (and stretches toward the new one) instead of switching off here\n  // and on there; a reflow (the rail becoming a row of tabs) places it without travel\n  const [itemsRef, plate] = useIndicator<HTMLDivElement>('[data-slot=\"item\"][data-state=\"selected\"]');\n\n  // ── the draft: edits over the saved values ─────────────────────────────────\n  const [saved, setSaved] = useControllable(values, defaultValues, onValuesChange);\n  const [edits, setEdits] = useControllable(draftProp, defaultDraft, onDraftChange);\n  const draft: SettingsValues = {\n    ...saved,\n    ...edits,\n    notifications: { ...saved.notifications, ...edits.notifications },\n    profile: { ...saved.profile, ...edits.profile },\n  };\n  const hasAvatar = saved.avatar !== undefined;\n  const hasAppearance = saved.appearance !== undefined;\n  const hasSecurity = saved.requireTwoFactor !== undefined || saved.allowedDomains !== undefined;\n  // with channels each notification has a switch per channel: \"digest.email\"\n  const notificationKeys = notificationChannels?.length\n    ? notificationOptions.flatMap((n) => notificationChannels.map((c) => `${n.id}.${c.id}`))\n    : notificationOptions.map((n) => n.id);\n  const changed = [\n    ...(Object.keys(localizedFIELD_NAMES(i18n)) as (keyof ReturnType<typeof localizedFIELD_NAMES>)[]).filter(\n      (k) => !same(draft[k as keyof SettingsValues], saved[k as keyof SettingsValues]),\n    ),\n    ...profileFields\n      .filter((field) => (draft.profile?.[field.id] ?? \"\") !== (saved.profile?.[field.id] ?? \"\"))\n      .map((field) => `profile.${field.id}`),\n    ...notificationKeys\n      .filter((key) => !!draft.notifications[key] !== !!saved.notifications[key])\n      .map((key) => `notifications.${key}`),\n  ];\n  const [saving, setSaving] = useState(false);\n  const [saveError, setSaveError] = useState<{ message: string; field?: string } | null>(null);\n  const [savedAt, setSavedAt] = useState<Date | null>(null);\n  const [showSaved, setShowSaved] = useExpiring<true>(1600);\n  const [announcement, setAnnouncement] = useState(\"\");\n  const saveBarRef = useRef<HTMLDivElement>(null);\n  const [toastBottom, setToastBottom] = useState(0);\n  const saveBarVisible = changed.length > 0 || showSaved;\n  const barState = saveError ? \"failed\" : saving ? \"saving\" : showSaved && !changed.length ? \"saved\" : \"dirty\";\n  useLayoutEffect(() => {\n    const bar = saveBarRef.current,\n      host = root.current;\n    if (!saveBarVisible || !bar || !host) return;\n    const measure = () =>\n      setToastBottom(Math.max(0, host.getBoundingClientRect().bottom - bar.getBoundingClientRect().top));\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(bar);\n    observer.observe(host);\n    host.addEventListener(\"scroll\", measure, true);\n    return () => {\n      observer.disconnect();\n      host.removeEventListener(\"scroll\", measure, true);\n    };\n  }, [saveBarVisible]);\n\n  const fieldSection = (key: string): SettingsSection =>\n    key.startsWith(\"notifications.\") ? \"notifications\" : WORKSPACE_FIELDS.includes(key) ? \"workspace\" : \"profile\";\n  const knownFields = [\n    \"locale\",\n    \"name\",\n    \"email\",\n    ...(hasAvatar ? [\"avatar\"] : []),\n    ...(hasWorkspace ? [\"workspace\", \"slug\"] : []),\n    ...profileFields.map((field) => `profile.${field.id}`),\n  ];\n  const clearFieldError = (key: string) => setSaveError((error) => (error?.field === key ? null : error));\n  const set = <K extends keyof SettingsValues>(key: K, value: SettingsValues[K]) => {\n    setEdits((edits) => ({ ...edits, [key]: value }));\n    setShowSaved(null);\n    clearFieldError(key);\n  };\n  const setProfile = (key: string, value: string) => {\n    setEdits((edits) => ({ ...edits, profile: { ...edits.profile, [key]: value } }));\n    setShowSaved(null);\n    clearFieldError(`profile.${key}`);\n  };\n  const errors: Record<string, string | undefined> = {\n    locale: draft.locale !== undefined && !validLocale(draft.locale) ? i18n(\"localeInvalid\") : undefined,\n    name: draft.name.trim() ? undefined : i18n(\"nameRequired\"),\n    email: isEmail(draft.email) ? undefined : i18n(\"emailInvalid\"),\n    ...(hasWorkspace\n      ? {\n          workspace: draft.workspace?.trim() ? undefined : i18n(\"workspaceRequired\"),\n          slug: /^[a-z0-9-]{3,32}$/.test(draft.slug ?? \"\") ? undefined : i18n(\"slugInvalid\"),\n        }\n      : {}),\n    ...Object.fromEntries(\n      profileFields.map((field) => {\n        const value = draft.profile?.[field.id] ?? \"\";\n        return [\n          `profile.${field.id}`,\n          !field.optional && !value.trim()\n            ? i18n(\"fieldRequired\", { field: field.label })\n            : field.optional && !value.trim()\n              ? undefined\n              : field.validate?.(value),\n        ];\n      }),\n    ),\n  };\n  const invalid = Object.values(errors).some(Boolean);\n  if (saveError?.field) errors[saveError.field] = saveError.message;\n  const focusHeading = () =>\n    root.current?.querySelector<HTMLElement>('[data-slot=\"settings-section-heading\"]')?.focus({ preventScroll: true });\n  const focusField = (field: string) =>\n    field === \"locale\"\n      ? localeField.current?.focus()\n      : root.current?.querySelector<HTMLElement>(`[data-settings-field=\"${CSS.escape(field)}\"]`)?.focus();\n  useEffect(() => {\n    const selected = root.current?.querySelector<HTMLElement>('[data-slot=\"item\"][data-state=\"selected\"]');\n    const row = selected?.parentElement;\n    if (selected && row && row.scrollWidth > row.clientWidth) {\n      const target = selected.getBoundingClientRect();\n      const viewport = row.getBoundingClientRect();\n      // Scroll this row only. scrollIntoView would also move the host page when the block is in a preview frame.\n      if (target.left < viewport.left) row.scrollBy({ left: target.left - viewport.left });\n      else if (target.right > viewport.right) row.scrollBy({ left: target.right - viewport.right });\n    }\n    if (pendingFocus.current) {\n      focusField(pendingFocus.current);\n      pendingFocus.current = null;\n    }\n  }, [section]);\n  const elapsed = useElapsed(saving);\n  useEffect(() => {\n    if (saving) {\n      setAnnouncement(i18n(\"saving\"));\n      return;\n    }\n    if (!changed.length) return;\n    const timer = setTimeout(() => setAnnouncement(i18n(\"unsavedCount\", { count: changed.length })), 500);\n    return () => clearTimeout(timer);\n  }, [changed.length, saving, i18n]);\n  async function save() {\n    if (invalid || saving) return;\n    const submitted = { ...draft, name: draft.name.trim(), email: draft.email.trim() };\n    const submittedEdits = edits;\n    setSaving(true);\n    setSaveError(null);\n    try {\n      await onSave(submitted, changed);\n      setSaved(submitted);\n      // A handler may be pending while the person keeps typing. Only remove the edits it actually saved.\n      setEdits((latest) => {\n        const remaining = { ...latest };\n        for (const key of Object.keys(submittedEdits) as (keyof SettingsValues)[]) {\n          if (key === \"profile\" || key === \"notifications\") {\n            const values: Record<string, string | boolean> = { ...latest[key] };\n            for (const id of Object.keys(submittedEdits[key] ?? {})) {\n              if (latest[key]?.[id] === submittedEdits[key]?.[id]) delete values[id];\n            }\n            if (Object.keys(values).length) Object.assign(remaining, { [key]: values });\n            else delete remaining[key];\n          } else if (latest[key] === submittedEdits[key]) delete remaining[key];\n        }\n        return remaining;\n      });\n      setSavedAt(new Date());\n      setShowSaved(true);\n      setAnnouncement(i18n(\"saved\"));\n      focusHeading();\n    } catch (error) {\n      const tagged = error as { field?: string };\n      const field = tagged?.field && knownFields.includes(tagged.field) ? tagged.field : undefined;\n      setSaveError({ message: message(error, i18n(\"saveFailed\")), field });\n      if (field && fieldSection(field) === section) requestAnimationFrame(() => focusField(field));\n    } finally {\n      setSaving(false);\n    }\n  }\n  const discard = () => {\n    setEdits({});\n    setSaveError(null);\n    setShowSaved(null);\n    setAnnouncement(i18n(\"changesDiscarded\"));\n    focusHeading();\n  };\n  const goToField = () => {\n    if (!saveError?.field) return;\n    pendingFocus.current = saveError.field;\n    setSection(fieldSection(saveError.field));\n  };\n\n  // ── photo and clock ───────────────────────────────────────────────────────\n  const photoInput = useRef<HTMLInputElement>(null);\n  const [photoError, setPhotoError] = useState<string | null>(null);\n  const [dropping, setDropping] = useState(false);\n  async function pickPhoto(file: File | undefined) {\n    if (!file) return;\n    if (!file.type.startsWith(\"image/\")) {\n      setPhotoError(i18n(\"photoNotImage\"));\n      return;\n    }\n    try {\n      set(\"avatar\", await squarePhoto(file));\n      setPhotoError(null);\n    } catch {\n      setPhotoError(i18n(\"photoUnreadable\"));\n    }\n  }\n  // the time zone field says what time it is there; it moves on at each new minute, not on a running timer. The clock\n  // is read only in the browser (useNow: null on the server and while hydrating), so a server-rendered page can show\n  // this screen: Next.js 16 refused to prerender it while the first render read Date.now() (3 Nov free-path run)\n  const firstNow = useNow();\n  const [tick, setTick] = useState<number | null>(null);\n  const now = tick ?? firstNow;\n  useEffect(() => {\n    if (now === null || section !== \"profile\" || !timeZones.length) return;\n    const timer = setTimeout(() => setTick(Date.now()), 60_000 - (Date.now() % 60_000) + 50);\n    return () => clearTimeout(timer);\n  }, [now, section, timeZones.length]);\n  const zone = zoneNow(draft.timeZone, now, i18n.locale);\n\n  // ── members ───────────────────────────────────────────────────────────────\n  const [members, setMembers] = useControllable(membersProp, defaultMembers ?? [], onMembersChange);\n  const [inviteOpen, setInviteOpen] = useState(false);\n  const [invite, setInvite] = useState<string[]>([]);\n  const [inviteRole, setInviteRole] = useState(\n    roles.find((r) => r.value === \"member\")?.value ?? roles.at(-1)?.value ?? \"\",\n  );\n  const [inviting, setInviting] = useState(false);\n  const [inviteError, setInviteError] = useState<string | null>(null);\n  const [removing, setRemoving] = useState<SettingsMember | null>(null);\n  const [busyRemove, setBusyRemove] = useState(false);\n  const roleLabel = (v: string) => roles.find((r) => r.value === v)?.label ?? v;\n  async function sendInvites() {\n    if (!onInvite || !invite.length) return;\n    setInviting(true);\n    setInviteError(null);\n    try {\n      const added = await onInvite(invite, inviteRole);\n      setMembers((current) => [...current, ...added]);\n      toast({\n        type: \"success\",\n        title: i18n(\"invitationsSent\", { count: added.length }),\n        description: i18n(\"invitationExpiry\"),\n      });\n      setInvite([]);\n      setInviteOpen(false);\n    } catch (e) {\n      setInviteError(message(e, i18n(\"inviteFailed\")));\n    } finally {\n      setInviting(false);\n    }\n  }\n  const roleRequests = useRef(new Map<string, symbol>());\n  async function changeRole(m: SettingsMember, role: string) {\n    const request = Symbol();\n    roleRequests.current.set(m.id, request);\n    setMembers((current) => current.map((x) => (x.id === m.id ? { ...x, role } : x)));\n    try {\n      await onRoleChange?.(m.id, role);\n      if (roleRequests.current.get(m.id) !== request) return;\n      toast({ type: \"success\", title: i18n(\"roleChanged\", { name: m.name, value: roleLabel(role) }) });\n    } catch (e) {\n      if (roleRequests.current.get(m.id) !== request) return;\n      setMembers((current) => current.map((x) => (x.id === m.id && x.role === role ? { ...x, role: m.role } : x)));\n      toast({\n        type: \"error\",\n        title: i18n(\"roleChangeFailed\", { name: m.name }),\n        description: message(e, i18n(\"retry\")),\n      });\n    } finally {\n      if (roleRequests.current.get(m.id) === request) roleRequests.current.delete(m.id);\n    }\n  }\n  async function removeMember() {\n    if (!removing) return;\n    setBusyRemove(true);\n    try {\n      await onRemoveMember?.(removing.id);\n      setMembers((current) => current.filter((x) => x.id !== removing.id));\n      toast({\n        type: \"success\",\n        title: removing.pending\n          ? i18n(\"invitationWithdrawn\", { email: removing.email })\n          : i18n(\"memberRemoved\", { name: removing.name }),\n      });\n      setRemoving(null);\n    } catch (e) {\n      toast({ type: \"error\", title: i18n(\"removeFailed\"), description: message(e, i18n(\"retry\")) });\n    } finally {\n      setBusyRemove(false);\n    }\n  }\n\n  // ── billing ───────────────────────────────────────────────────────────────\n  const [plan, setPlan] = useControllable(planProp, defaultPlan ?? plans?.[0]?.id ?? \"\", undefined);\n  const [period, setPeriod] = useControllable<BillingPeriod>(periodProp, defaultPeriod, onPeriodChange);\n  const [choice, setChoice] = useState<{ plan: string; period: BillingPeriod } | null>(null);\n  const [switching, setSwitching] = useState(false);\n  const wanted = choice ?? { plan, period };\n  const pending = wanted.plan !== plan || wanted.period !== period;\n  const planName = (id: string) => plans?.find((p) => p.id === id)?.name ?? id;\n  const yearlySaving = Math.max(\n    0,\n    ...(plans ?? []).map((p) =>\n      p.price && p.price.monthly > 0 ? Math.round((1 - p.price.yearly / p.price.monthly) * 100) : 0,\n    ),\n  );\n  async function switchPlan() {\n    setSwitching(true);\n    try {\n      await onPlanChange?.(wanted.plan, wanted.period);\n      setPlan(wanted.plan);\n      setPeriod(wanted.period);\n      setChoice(null);\n      toast({\n        type: \"success\",\n        title: i18n(\"planChanged\", {\n          value: planName(wanted.plan),\n          period: i18n(wanted.period === \"monthly\" ? \"monthlyPeriod\" : \"yearlyPeriod\"),\n        }),\n      });\n    } catch (e) {\n      toast({ type: \"error\", title: i18n(\"planChangeFailed\"), description: message(e, i18n(\"retry\")) });\n    } finally {\n      setSwitching(false);\n    }\n  }\n\n  // ── API keys ──────────────────────────────────────────────────────────────\n  const [keys, setKeys] = useControllable(keysProp, defaultApiKeys ?? [], onApiKeysChange);\n  const [keyOpen, setKeyOpen] = useState(false);\n  const [keyName, setKeyName] = useState(\"\");\n  const [creating, setCreating] = useState(false);\n  const [keyError, setKeyError] = useState<string | null>(null);\n  const [secret, setSecret] = useState<{ name: string; value: string } | null>(null);\n  const [revoking, setRevoking] = useState<SettingsApiKey | null>(null);\n  const [busyRevoke, setBusyRevoke] = useState(false);\n  const changeKeyOpen = (open: boolean) => {\n    setKeyOpen(open);\n    if (!open) {\n      setSecret(null);\n      setKeyError(null);\n      setKeyName(\"\");\n    }\n  };\n  async function createKey() {\n    if (!onCreateKey) return;\n    if (!keyName.trim()) {\n      setKeyError(i18n(\"keyNameRequired\"));\n      return;\n    }\n    setCreating(true);\n    setKeyError(null);\n    try {\n      const { key, secret: value } = await onCreateKey(keyName.trim());\n      setKeys((current) => [...current, key]);\n      setSecret({ name: key.name, value });\n      setKeyName(\"\");\n    } catch (e) {\n      setKeyError(message(e, i18n(\"keyFailed\")));\n    } finally {\n      setCreating(false);\n    }\n  }\n  async function revokeKey() {\n    if (!revoking) return;\n    setBusyRevoke(true);\n    try {\n      await onRevokeKey?.(revoking.id);\n      setKeys((current) => current.filter((k) => k.id !== revoking.id));\n      toast({\n        type: \"success\",\n        title: i18n(\"revoked\", { name: revoking.name }),\n        description: i18n(\"keyRevokedDescription\"),\n      });\n      setRevoking(null);\n    } catch (e) {\n      toast({\n        type: \"error\",\n        title: i18n(\"keyRevokeFailed\", { name: revoking.name }),\n        description: message(e, i18n(\"retry\")),\n      });\n    } finally {\n      setBusyRevoke(false);\n    }\n  }\n\n  // ── danger zone ───────────────────────────────────────────────────────────\n  const [deletion, setDeletion] = useState<\"idle\" | \"working\" | \"scheduled\" | \"failed\">(\"idle\");\n  const [deleteError, setDeleteError] = useState<string | null>(null);\n  async function deleteWorkspace() {\n    if (!onDeleteWorkspace) return;\n    setDeletion(\"working\");\n    setDeleteError(null);\n    try {\n      await onDeleteWorkspace();\n      setDeletion(\"scheduled\");\n      toast({\n        type: \"warning\",\n        title: i18n(\"deletionScheduledToast\"),\n        description: i18n(\"deletionCancellationWindow\"),\n      });\n    } catch (e) {\n      setDeletion(\"failed\");\n      setDeleteError(message(e, i18n(\"deleteFailed\")));\n    }\n  }\n\n  const active = members.filter((m) => !m.pending).length;\n  const invited = members.length - active;\n\n  return (\n    <div\n      ref={composedRef}\n      style={style}\n      data-slot=\"settings\"\n      data-navigation={navigation}\n      data-section={section}\n      className={cn(\n        \"@container relative min-h-0 text-ink\",\n        // its own screen fills the height and scrolls inside; inside your page it flows with the page\n        navigation !== \"none\" && \"h-full bg-canvas\",\n        styles.root,\n        classNames?.root,\n        className,\n      )}\n    >\n      <div data-slot=\"settings-layout\" className={cn(styles.layout, classNames?.layout)}>\n        {navigation !== \"none\" && (\n          <nav\n            data-slot=\"settings-navigation\"\n            aria-label={i18n(\"navigationLabel\")}\n            className={cn(styles.navigation, classNames?.navigation)}\n          >\n            {heading != null && (\n              <div data-slot=\"settings-heading\" className={cn(styles.heading, classNames?.heading)}>\n                <h1\n                  data-slot=\"settings-title\"\n                  className={cn(\n                    \"font-display text-2xl font-title tracking-[var(--pui-tracking-display)]\",\n                    classNames?.title,\n                  )}\n                >\n                  {heading}\n                </h1>\n                {headingAside}\n              </div>\n            )}\n            {hasWorkspace && (\n              // whose settings these are: the workspace, its plan and its people, above the list\n              <div data-slot=\"settings-identity\" className={cn(styles.identity, classNames?.identity)}>\n                <Avatar className={classNames?.avatar} name={saved.workspace || \"?\"} shape=\"square\" size=\"md\" />\n                <span className=\"min-w-0\">\n                  <span\n                    data-slot=\"settings-identityName\"\n                    className={cn(\"block truncate text-sm text-ink\", classNames?.identityName)}\n                  >\n                    {saved.workspace}\n                  </span>\n                  <span\n                    data-slot=\"settings-identityMeta\"\n                    className={cn(\"block truncate text-xs text-ink-3\", classNames?.identityMeta)}\n                  >\n                    {[\n                      plans?.length ? planName(plan) : null,\n                      hasMembers ? i18n(\"identityMembers\", { count: active }) : null,\n                    ]\n                      .filter(Boolean)\n                      .join(\" · \") || domain}\n                  </span>\n                </span>\n              </div>\n            )}\n            <div ref={itemsRef} data-slot=\"settings-items\" className={cn(styles.items, classNames?.items)}>\n              {plate && (\n                <span\n                  aria-hidden=\"true\"\n                  data-slot=\"settings-plate\"\n                  className={cn(\"glide glide-2d\", styles.plate, classNames?.plate)}\n                  {...glideProps(plate)}\n                  data-ready={plate.ready && plate.cause === \"choice\" ? \"\" : undefined}\n                />\n              )}\n              {sections.map((s) => [\n                // in the rail the list falls into the person's own sections and the workspace's\n                hasWorkspace && (s === \"profile\" || s === \"workspace\") && (\n                  <span\n                    key={`group-${s}`}\n                    aria-hidden=\"true\"\n                    data-slot=\"settings-group\"\n                    className={cn(\"text-overline text-ink-3\", styles.group, classNames?.group)}\n                  >\n                    {s === \"profile\" ? i18n(\"groupAccount\") : i18n(\"workspace\")}\n                  </span>\n                ),\n                <Item\n                  key={s}\n                  size=\"sm\"\n                  media={ICONS[s]}\n                  title={\n                    <>\n                      {sectionLabels[s]}\n                      {changed.some((key) => fieldSection(key) === s) && (\n                        <span data-slot=\"settings-unsavedLabel\" className={cn(\"sr-only\", classNames?.unsavedLabel)}>\n                          {\" \"}\n                          {i18n(\"unsaved\")}\n                        </span>\n                      )}\n                    </>\n                  }\n                  meta={\n                    saveError?.field && fieldSection(saveError.field) === s ? (\n                      <span\n                        data-slot=\"settings-fieldAttention\"\n                        className={cn(\"text-red\", classNames?.fieldAttention)}\n                        aria-label={i18n(\"fieldNeedsAttention\")}\n                      >\n                        ▲\n                      </span>\n                    ) : changed.some((key) => fieldSection(key) === s) ? (\n                      <span\n                        data-slot=\"settings-dirty\"\n                        data-state=\"dirty\"\n                        className={cn(\"block size-1.5 rounded-full bg-accent\", classNames?.dirty)}\n                        aria-hidden\n                      />\n                    ) : undefined\n                  }\n                  selected={section === s}\n                  onSelect={() => setSection(s)}\n                  className={cn(\"shrink-0\", styles.item, classNames?.item)}\n                />,\n              ])}\n            </div>\n          </nav>\n        )}\n\n        <div\n          data-slot=\"settings-viewport\"\n          className={cn(\n            \"relative flex min-h-0 min-w-0 flex-col\",\n            // inside your page the page scrolls, so the save bar floats at the bottom of the window while you edit\n            navigation !== \"none\" && \"overflow-y-auto\",\n            classNames?.viewport,\n          )}\n        >\n          <div data-slot=\"settings-body\" className={cn(\"min-h-0 flex-1\", classNames?.body)}>\n            <SavedCaption.Provider\n              value={\n                savedAt && (\n                  <p\n                    data-slot=\"settings-saved-at\"\n                    className={cn(\"text-caption text-ink-3 tabular-nums\", classNames?.savedAt)}\n                  >\n                    {i18n(\"savedAt\", { value: i18n.date(savedAt, { hour: \"2-digit\", minute: \"2-digit\" }) })}\n                  </p>\n                )\n              }\n            >\n              <FlatPanels.Provider value={navigation === \"none\"}>\n                <div data-slot=\"settings-column\" className={cn(styles.column, classNames?.column)}>\n                  {section === \"profile\" && (\n                    <Section\n                      classNames={classNames}\n                      title={i18n(\"profile\")}\n                      description={hasAppearance ? i18n(\"profileAppearanceDescription\") : i18n(\"profileDescription\")}\n                    >\n                      <Panel classNames={classNames}>\n                        <div className={cn(hasAvatar || onPhotoChange ? styles.identityGrid : \"grid gap-4\")}>\n                          <div\n                            data-slot=\"settings-photoRow\"\n                            data-state={dropping ? \"dropping\" : undefined}\n                            className={cn(styles.photo, navigation !== \"none\" && \"m-inset\", classNames?.photoRow)}\n                            onDragOver={\n                              hasAvatar\n                                ? (e) => {\n                                    e.preventDefault();\n                                    setDropping(true);\n                                  }\n                                : undefined\n                            }\n                            onDragLeave={hasAvatar ? () => setDropping(false) : undefined}\n                            onDrop={\n                              hasAvatar\n                                ? (e) => {\n                                    e.preventDefault();\n                                    setDropping(false);\n                                    void pickPhoto(e.dataTransfer.files[0]);\n                                  }\n                                : undefined\n                            }\n                          >\n                            <Avatar\n                              // a new photo arrives (keyed by it, so typing a name never replays this)\n                              key={draft.avatar || \"initials\"}\n                              className={cn(styles.photoAvatar, draft.avatar && \"enter-scale\", classNames?.avatar)}\n                              name={draft.name || \"?\"}\n                              src={draft.avatar || undefined}\n                              size=\"xl\"\n                            />\n                            {hasAvatar ? (\n                              <div\n                                data-slot=\"settings-photoActions\"\n                                className={cn(\"flex flex-wrap gap-1\", classNames?.photoActions)}\n                              >\n                                <input\n                                  ref={photoInput}\n                                  type=\"file\"\n                                  accept=\"image/*\"\n                                  hidden\n                                  data-settings-field=\"avatar\"\n                                  onChange={(e) => {\n                                    void pickPhoto(e.target.files?.[0]);\n                                    e.target.value = \"\";\n                                  }}\n                                />\n                                <Button\n                                  className={classNames?.button}\n                                  variant=\"secondary\"\n                                  size=\"sm\"\n                                  onClick={() => photoInput.current?.click()}\n                                >\n                                  <ImageUp className=\"size-4\" />\n                                  {draft.avatar ? i18n(\"photoChange\") : i18n(\"photoUpload\")}\n                                </Button>\n                                {draft.avatar && (\n                                  <Button\n                                    className={classNames?.button}\n                                    variant=\"ghost\"\n                                    size=\"sm\"\n                                    onClick={() => set(\"avatar\", \"\")}\n                                  >\n                                    {i18n(\"remove\")}\n                                  </Button>\n                                )}\n                              </div>\n                            ) : (\n                              onPhotoChange && (\n                                <Button\n                                  className={classNames?.button}\n                                  variant=\"secondary\"\n                                  size=\"sm\"\n                                  onClick={onPhotoChange}\n                                >\n                                  {i18n(\"changePhoto\")}\n                                </Button>\n                              )\n                            )}\n                            {hasAvatar &&\n                              (photoError || errors.avatar ? (\n                                <p\n                                  data-slot=\"settings-photoError\"\n                                  role=\"alert\"\n                                  className={cn(\"text-xs text-red\", classNames?.photoError)}\n                                >\n                                  {photoError ?? errors.avatar}\n                                </p>\n                              ) : (\n                                <p\n                                  data-slot=\"settings-photoHint\"\n                                  className={cn(\"text-xs text-ink-3\", classNames?.photoHint)}\n                                >\n                                  {i18n(\"photoHint\")}\n                                </p>\n                              ))}\n                          </div>\n                          <div className={styles.fields}>\n                            <Input\n                              className={classNames?.name}\n                              label={i18n(\"name\")}\n                              data-settings-field=\"name\"\n                              required\n                              autoComplete=\"name\"\n                              value={draft.name}\n                              onChange={(e) => set(\"name\", e.target.value)}\n                              error={errors.name}\n                            />\n                            <Input\n                              className={classNames?.email}\n                              label={i18n(\"email\")}\n                              data-settings-field=\"email\"\n                              required\n                              dir=\"ltr\"\n                              type=\"email\"\n                              autoComplete=\"email\"\n                              value={draft.email}\n                              onChange={(e) => set(\"email\", e.target.value)}\n                              error={errors.email}\n                              description={i18n(\"emailDescription\")}\n                            />\n                            {profileFields.map((field) => (\n                              <Input\n                                className={classNames?.profileField}\n                                key={field.id}\n                                label={\n                                  <>\n                                    {field.label}\n                                    {field.optional && (\n                                      <span\n                                        data-slot=\"settings-optional\"\n                                        className={cn(\"text-ink-3\", classNames?.optional)}\n                                      >\n                                        {\" \"}\n                                        {i18n(\"optional\")}\n                                      </span>\n                                    )}\n                                  </>\n                                }\n                                data-settings-field={`profile.${field.id}`}\n                                name={field.id}\n                                required={!field.optional}\n                                autoComplete={field.autoComplete}\n                                maxLength={field.maxLength}\n                                description={field.description}\n                                value={draft.profile?.[field.id] ?? \"\"}\n                                onChange={(event) => setProfile(field.id, event.target.value)}\n                                error={errors[`profile.${field.id}`]}\n                              />\n                            ))}\n                            {timeZones.length > 0 && (\n                              <Combobox\n                                className={classNames?.timeZone}\n                                label={i18n(\"timeZone\")}\n                                options={timeZones.map((z) => {\n                                  const there = zoneNow(z, now, i18n.locale);\n                                  return {\n                                    value: z,\n                                    label: z.replace(/_/g, \" \"),\n                                    description: there ? `${there.time} · ${there.offset}` : undefined,\n                                    keywords: there ? [there.offset] : undefined,\n                                  };\n                                })}\n                                value={draft.timeZone}\n                                onValueChange={(v) => v && set(\"timeZone\", v)}\n                                description={\n                                  zone ? (\n                                    <span\n                                      data-slot=\"settings-zone\"\n                                      className={cn(\"inline-flex items-center gap-1.5\", classNames?.zone)}\n                                    >\n                                      <Clock className=\"size-3.5\" aria-hidden=\"true\" />\n                                      <span className=\"tabular-nums\">\n                                        {i18n(\"zoneNow\", { time: zone.time, offset: zone.offset })}\n                                      </span>\n                                    </span>\n                                  ) : (\n                                    i18n(\"timeZoneDescription\")\n                                  )\n                                }\n                              />\n                            )}\n                            {(draft.locale !== undefined || localeOptionsProp !== undefined) && (\n                              <Select\n                                ref={localeField}\n                                label={i18n(\"locale\")}\n                                description={i18n(\"localeDescription\")}\n                                name=\"locale\"\n                                value={draft.locale ?? i18n.locale}\n                                onValueChange={(value) => set(\"locale\", value)}\n                                error={errors.locale}\n                                options={[\n                                  ...localeOptions,\n                                  ...(!localeOptions.some((o) => o.value === (draft.locale ?? i18n.locale))\n                                    ? [{ value: draft.locale ?? i18n.locale }]\n                                    : []),\n                                ].map((option) => ({\n                                  value: option.value,\n                                  label:\n                                    option.label ??\n                                    (validLocale(option.value)\n                                      ? new Intl.DisplayNames(i18n.locale, { type: \"language\" }).of(option.value)\n                                      : option.value) ??\n                                    option.value,\n                                }))}\n                              />\n                            )}\n                          </div>\n                        </div>\n                      </Panel>\n                      {hasAppearance && (\n                        <Panel\n                          classNames={classNames}\n                          title={i18n(\"appearance\")}\n                          description={i18n(\"appearanceDescription\")}\n                        >\n                          <RadioCards\n                            showLabel={false}\n                            label={i18n(\"appearance\")}\n                            className={cn(styles.appearance, classNames?.appearance)}\n                            layout=\"grid\"\n                            value={draft.appearance}\n                            onValueChange={(v) => set(\"appearance\", v)}\n                            cards={appearanceOptions.map((a) => ({\n                              value: a.value,\n                              label: a.label,\n                              detail: a.detail,\n                              media: a.preview ?? <SchemePreview scheme={a.value} className={classNames?.preview} />,\n                            }))}\n                          />\n                        </Panel>\n                      )}\n                    </Section>\n                  )}\n\n                  {section === \"workspace\" && (\n                    <Section\n                      classNames={classNames}\n                      title={i18n(\"workspace\")}\n                      description={i18n(\"workspaceDescription\", { workspace: saved.workspace ?? \"\" })}\n                    >\n                      <Panel classNames={classNames}>\n                        <div className={styles.fields}>\n                          <Input\n                            className={classNames?.workspace}\n                            label={i18n(\"workspaceName\")}\n                            data-settings-field=\"workspace\"\n                            value={draft.workspace ?? \"\"}\n                            onChange={(e) => set(\"workspace\", e.target.value)}\n                            error={errors.workspace}\n                          />\n                          <Input\n                            className={classNames?.slug}\n                            label={i18n(\"workspaceUrl\")}\n                            data-settings-field=\"slug\"\n                            dir=\"ltr\"\n                            value={draft.slug ?? \"\"}\n                            onChange={(e) => set(\"slug\", e.target.value.toLowerCase())}\n                            leading={\n                              <span data-slot=\"settings-domain\" className={cn(\"text-ink-3\", classNames?.domain)}>\n                                {domain}/\n                              </span>\n                            }\n                            error={errors.slug}\n                            description={i18n(\"workspaceUrlDescription\")}\n                          />\n                        </div>\n                      </Panel>\n                      {hasSecurity && (\n                        <Panel\n                          classNames={classNames}\n                          title={i18n(\"security\")}\n                          description={i18n(\"securityDescription\")}\n                          className={classNames?.security}\n                        >\n                          {saved.requireTwoFactor !== undefined && (\n                            <Switch\n                              label={i18n(\"requireTwoFactor\")}\n                              description={i18n(\"requireTwoFactorDescription\")}\n                              labelPosition=\"start\"\n                              checked={!!draft.requireTwoFactor}\n                              onCheckedChange={(v) => set(\"requireTwoFactor\", v)}\n                            />\n                          )}\n                          {saved.allowedDomains !== undefined && (\n                            <TagInput\n                              label={i18n(\"signInDomains\")}\n                              description={i18n(\"signInDomainsDescription\")}\n                              value={draft.allowedDomains ?? []}\n                              onValueChange={(v) => set(\"allowedDomains\", v)}\n                              placeholder={i18n(\"signInDomainsPlaceholder\")}\n                              validate={(v) =>\n                                /^([a-z0-9-]+\\.)+[a-z]{2,}$/i.test(v.trim()) ? undefined : i18n(\"domainInvalid\")\n                              }\n                              error={errors.allowedDomains}\n                            />\n                          )}\n                        </Panel>\n                      )}\n                    </Section>\n                  )}\n\n                  {section === \"members\" && (\n                    <Section\n                      classNames={classNames}\n                      title={i18n(\"members\")}\n                      description={\n                        invited\n                          ? i18n(\"membersInvitedDescription\", { active, count: invited })\n                          : i18n(\"membersDescription\", { count: active })\n                      }\n                      action={\n                        onInvite && (\n                          <Dialog\n                            open={inviteOpen}\n                            onOpenChange={(o) => {\n                              setInviteOpen(o);\n                              if (!o) setInviteError(null);\n                            }}\n                          >\n                            <DialogTrigger asChild>\n                              <Button className={classNames?.button} size=\"sm\">\n                                <Plus\n                                  data-slot=\"settings-inviteIcon\"\n                                  className={cn(\"size-4\", classNames?.inviteIcon)}\n                                />\n                                {i18n(\"invite\")}\n                              </Button>\n                            </DialogTrigger>\n                            <DialogContent\n                              className={classNames?.dialog}\n                              title={i18n(\"inviteTitle\")}\n                              description={i18n(\"inviteDescription\")}\n                              dismissible={!inviting}\n                              footer={\n                                <>\n                                  <Button\n                                    className={classNames?.button}\n                                    variant=\"ghost\"\n                                    onClick={() => setInviteOpen(false)}\n                                    disabled={inviting}\n                                  >\n                                    {i18n(\"cancel\")}\n                                  </Button>\n                                  <Button\n                                    className={classNames?.button}\n                                    disabled={!invite.length}\n                                    loading={inviting}\n                                    onClick={sendInvites}\n                                  >\n                                    {i18n(\"sendInvites\", { count: invite.length })}\n                                  </Button>\n                                </>\n                              }\n                            >\n                              <div data-slot=\"settings-inviteForm\" className={cn(\"grid gap-4\", classNames?.inviteForm)}>\n                                <TagInput\n                                  className={classNames?.inviteEmails}\n                                  label={i18n(\"inviteEmails\")}\n                                  value={invite}\n                                  onValueChange={setInvite}\n                                  placeholder={i18n(\"invitePlaceholder\")}\n                                  validate={(v) =>\n                                    isEmail(v)\n                                      ? members.some((m) => m.email === v.trim())\n                                        ? i18n(\"alreadyMember\")\n                                        : undefined\n                                      : i18n(\"emailRequired\")\n                                  }\n                                />\n                                <Select\n                                  className={classNames?.role}\n                                  label={i18n(\"role\")}\n                                  options={roles.filter((r) => r.value !== \"owner\")}\n                                  value={inviteRole}\n                                  onValueChange={setInviteRole}\n                                />\n                                {inviteError && (\n                                  <p\n                                    data-slot=\"settings-inviteError\"\n                                    role=\"alert\"\n                                    className={cn(\"text-sm text-red\", classNames?.inviteError)}\n                                  >\n                                    {inviteError}\n                                  </p>\n                                )}\n                              </div>\n                            </DialogContent>\n                          </Dialog>\n                        )\n                      }\n                    >\n                      <Panel classNames={classNames} flush>\n                        <Table className={classNames?.table}>\n                          <TableHeader className={classNames?.tableHeader}>\n                            <TableRow className={classNames?.tableRow}>\n                              <TableHead>{i18n(\"person\")}</TableHead>\n                              <TableHead className={cn(\"w-[160px]\", classNames?.roleHeader)}>{i18n(\"role\")}</TableHead>\n                              <TableHead className={cn(\"w-10\", classNames?.actionsHeader)}>\n                                <span\n                                  data-slot=\"settings-actionsLabel\"\n                                  className={cn(\"sr-only\", classNames?.actionsLabel)}\n                                >\n                                  {i18n(\"actions\")}\n                                </span>\n                              </TableHead>\n                            </TableRow>\n                          </TableHeader>\n                          <TableBody className={classNames?.tableBody}>\n                            {members.map((m) => (\n                              <TableRow\n                                className={cn(\"reveal-host\", classNames?.tableRow)}\n                                key={m.id}\n                                data-member={m.id}\n                              >\n                                <TableCell className={cn(\"min-w-0\", classNames?.personCell)}>\n                                  <span\n                                    data-slot=\"settings-person\"\n                                    className={cn(\"flex min-w-0 items-center gap-3\", classNames?.person)}\n                                  >\n                                    <Avatar className={classNames?.avatar} name={m.name} src={m.avatar} size=\"sm\" />\n                                    <span\n                                      data-slot=\"settings-personBody\"\n                                      className={cn(\"flex min-w-0 flex-col\", classNames?.personBody)}\n                                    >\n                                      <span\n                                        data-slot=\"settings-personName\"\n                                        className={cn(\n                                          \"flex items-center gap-1.5 truncate text-ink\",\n                                          classNames?.personName,\n                                        )}\n                                      >\n                                        {m.name}\n                                        {m.pending && (\n                                          <Badge className={classNames?.badge} tone=\"warning\" size=\"sm\">\n                                            {i18n(\"invited\")}\n                                          </Badge>\n                                        )}\n                                      </span>\n                                      <span\n                                        data-slot=\"settings-personEmail\"\n                                        className={cn(\"truncate text-xs text-ink-3\", classNames?.personEmail)}\n                                      >\n                                        {m.email}\n                                      </span>\n                                    </span>\n                                  </span>\n                                </TableCell>\n                                <TableCell>\n                                  {m.role === \"owner\" || !onRoleChange ? (\n                                    <span\n                                      data-slot=\"settings-roleLabel\"\n                                      className={cn(\"text-sm text-ink-2\", classNames?.roleLabel)}\n                                    >\n                                      {roleLabel(m.role)}\n                                    </span>\n                                  ) : (\n                                    <Select\n                                      className={classNames?.role}\n                                      label={i18n(\"memberRoleLabel\", { name: m.name })}\n                                      showLabel={false}\n                                      size=\"sm\"\n                                      options={roles.filter((r) => r.value !== \"owner\")}\n                                      value={m.role}\n                                      onValueChange={(role) => changeRole(m, role)}\n                                    />\n                                  )}\n                                </TableCell>\n                                <TableCell>\n                                  {/* a row's actions wait for attention (hover, focus, an open menu; always on touch) */}\n                                  {m.role !== \"owner\" && onRemoveMember && (\n                                    <span data-slot=\"settings-memberActionsReveal\" className=\"reveal inline-flex\">\n                                      <DropdownMenu\n                                        className={classNames?.memberActions}\n                                        label={i18n(\"memberActionsLabel\", { name: m.name })}\n                                        icon={\n                                          <MoreHorizontal\n                                            data-slot=\"settings-memberActionsIcon\"\n                                            className={cn(\"size-4\", classNames?.memberActionsIcon)}\n                                          />\n                                        }\n                                        items={[\n                                          {\n                                            label: m.pending ? i18n(\"withdrawInvitation\") : i18n(\"removeFromWorkspace\"),\n                                            tone: \"danger\",\n                                            onSelect: () => setRemoving(m),\n                                          },\n                                        ]}\n                                      />\n                                    </span>\n                                  )}\n                                </TableCell>\n                              </TableRow>\n                            ))}\n                          </TableBody>\n                        </Table>\n                      </Panel>\n                      <AlertDialog\n                        open={!!removing}\n                        onOpenChange={(o) => {\n                          if (!o && !busyRemove) setRemoving(null);\n                        }}\n                      >\n                        {removing && (\n                          <AlertDialogContent\n                            className={classNames?.confirmation}\n                            title={\n                              removing.pending\n                                ? i18n(\"withdrawTitle\", { email: removing.email })\n                                : i18n(\"removeTitle\", { name: removing.name })\n                            }\n                            description={removing.pending ? i18n(\"withdrawDescription\") : i18n(\"removeDescription\")}\n                          >\n                            <AlertDialogFooter className={classNames?.confirmationFooter}>\n                              <AlertDialogCancel className={classNames?.confirmationCancel} disabled={busyRemove}>\n                                {i18n(\"cancel\")}\n                              </AlertDialogCancel>\n                              <AlertDialogAction\n                                className={classNames?.confirmationAction}\n                                loading={busyRemove}\n                                onClick={(e) => {\n                                  e.preventDefault();\n                                  removeMember();\n                                }}\n                              >\n                                {removing.pending ? i18n(\"withdraw\") : i18n(\"remove\")}\n                              </AlertDialogAction>\n                            </AlertDialogFooter>\n                          </AlertDialogContent>\n                        )}\n                      </AlertDialog>\n                    </Section>\n                  )}\n\n                  {section === \"billing\" && plans && (\n                    <Section\n                      classNames={classNames}\n                      title={i18n(\"billing\")}\n                      description={i18n(\"billingDescription\")}\n                      action={\n                        <SegmentedControl\n                          showLabel={false}\n                          label={i18n(\"billingPeriod\")}\n                          size=\"sm\"\n                          className={classNames?.period}\n                          value={wanted.period}\n                          onValueChange={(p) => setChoice({ plan: wanted.plan, period: p as BillingPeriod })}\n                          options={[\n                            { value: \"monthly\", label: i18n(\"monthly\") },\n                            {\n                              value: \"yearly\",\n                              label: i18n(\"yearly\"),\n                              note: yearlySaving ? i18n.number(-yearlySaving / 100, { style: \"percent\" }) : undefined,\n                              activeNote: yearlySaving\n                                ? i18n(\"savingDiscount\", {\n                                    percent: i18n.number(yearlySaving / 100, { style: \"percent\" }),\n                                  })\n                                : undefined,\n                            },\n                          ]}\n                        />\n                      }\n                    >\n                      <RadioCards\n                        showLabel={false}\n                        className={classNames?.plans}\n                        label={i18n(\"plan\")}\n                        value={wanted.plan}\n                        onValueChange={(p) => setChoice({ plan: p, period: wanted.period })}\n                        cards={plans.map((p) => ({\n                          value: p.id,\n                          detail: p.detail,\n                          label: (\n                            <span\n                              data-slot=\"settings-planName\"\n                              className={cn(\"inline-flex items-center gap-1.5\", classNames?.planName)}\n                            >\n                              {p.name}\n                              {p.id === plan && (\n                                <Badge className={classNames?.badge} tone=\"accent\" size=\"sm\">\n                                  {i18n(\"current\")}\n                                </Badge>\n                              )}\n                            </span>\n                          ),\n                          // the price rolls when the period changes, as it does on the plan comparison\n                          figure: p.price ? (\n                            <>\n                              <span dir=\"ltr\">\n                                <SlotText\n                                  value={p.price[wanted.period]}\n                                  format={(v) =>\n                                    i18n.number(v, {\n                                      style: \"currency\",\n                                      currency,\n                                      minimumFractionDigits: 0,\n                                      maximumFractionDigits: 2,\n                                    })\n                                  }\n                                />\n                              </span>{\" \"}\n                              {i18n(\"priceUnit\")}\n                            </>\n                          ) : (\n                            i18n(\"contactSales\")\n                          ),\n                        }))}\n                      />\n                      {/* the pending switch grows in under the plans, so the meters below glide down */}\n                      {pending && (\n                        <div className=\"enter-grow\">\n                          <div\n                            data-slot=\"settings-planChange\"\n                            role=\"region\"\n                            aria-label={i18n(\"planChange\")}\n                            className={cn(\n                              \"m-surface flex flex-wrap items-center gap-3 rounded-card px-4 py-3\",\n                              classNames?.planChange,\n                            )}\n                          >\n                            <p\n                              data-slot=\"settings-planDescription\"\n                              className={cn(\"me-auto text-sm text-ink-2\", classNames?.planDescription)}\n                            >\n                              <span data-slot=\"settings-wantedPlan\" className={cn(\"text-ink\", classNames?.wantedPlan)}>\n                                {i18n(\"planChangeDescription\", {\n                                  plan: planName(wanted.plan),\n                                  period: i18n(wanted.period === \"monthly\" ? \"monthlyPeriod\" : \"yearlyPeriod\"),\n                                })}\n                              </span>\n                            </p>\n                            <Button\n                              className={classNames?.button}\n                              variant=\"ghost\"\n                              size=\"sm\"\n                              onClick={() => setChoice(null)}\n                              disabled={switching}\n                            >\n                              {i18n(\"keepCurrent\")}\n                            </Button>\n                            <Button\n                              className={classNames?.button}\n                              size=\"sm\"\n                              loading={switching}\n                              onClick={switchPlan}\n                              disabled={!onPlanChange}\n                            >\n                              {i18n(\"switchPlan\")}\n                            </Button>\n                          </div>\n                        </div>\n                      )}\n                      {(seats != null || usage.length > 0) && (\n                        <Panel classNames={classNames} title={i18n(\"usage\")} description={i18n(\"usageDescription\")}>\n                          <div className={styles.meters}>\n                            {seats != null && (\n                              <UsageMeter\n                                className={classNames?.usage}\n                                label={i18n(\"seats\")}\n                                limit={seats}\n                                segments={[\n                                  { id: \"active\", label: i18n(\"active\"), value: active },\n                                  { id: \"invited\", label: i18n(\"invited\"), value: invited },\n                                ]}\n                              />\n                            )}\n                            {usage.map((u) => (\n                              <UsageMeter\n                                className={classNames?.usage}\n                                key={u.id}\n                                label={u.label}\n                                unit={u.unit}\n                                limit={u.limit}\n                                warnAt={u.warnAt}\n                                segments={u.segments}\n                              />\n                            ))}\n                          </div>\n                        </Panel>\n                      )}\n                    </Section>\n                  )}\n\n                  {section === \"notifications\" && (\n                    <Section\n                      classNames={classNames}\n                      title={i18n(\"notifications\")}\n                      description={\n                        notificationChannels?.length\n                          ? i18n(\"notificationsByChannelDescription\")\n                          : i18n(\"notificationsDescription\")\n                      }\n                    >\n                      {notificationChannels?.length ? (\n                        // one switch per event and channel: rows are what happened, columns are where it reaches you\n                        <Panel classNames={classNames} flush>\n                          <table\n                            data-slot=\"settings-matrix\"\n                            aria-label={i18n(\"notificationsByChannel\")}\n                            className={cn(styles.matrix, classNames?.matrix)}\n                          >\n                            <thead>\n                              <tr>\n                                <th scope=\"col\">\n                                  <span className=\"sr-only\">{i18n(\"event\")}</span>\n                                </th>\n                                {notificationChannels.map((c) => (\n                                  <th key={c.id} scope=\"col\">\n                                    <span className={styles.channel}>\n                                      {c.icon ?? SETTINGS_CHANNEL_ICONS[c.id]}\n                                      {c.label}\n                                    </span>\n                                  </th>\n                                ))}\n                              </tr>\n                            </thead>\n                            <tbody>\n                              {notificationOptions.map((n) => (\n                                <tr key={n.id} data-slot=\"settings-notification\" className={classNames?.notification}>\n                                  <th scope=\"row\">\n                                    <span\n                                      data-slot=\"settings-notificationLabel\"\n                                      className={cn(\"block text-sm text-ink\", classNames?.notificationLabel)}\n                                    >\n                                      {n.label}\n                                    </span>\n                                    <span\n                                      data-slot=\"settings-notificationDescription\"\n                                      className={cn(\"block text-xs text-ink-3\", classNames?.notificationDescription)}\n                                    >\n                                      {n.description}\n                                    </span>\n                                  </th>\n                                  {notificationChannels.map((c) => {\n                                    const key = `${n.id}.${c.id}`;\n                                    return (\n                                      <td key={c.id}>\n                                        <Switch\n                                          className={classNames?.notificationSwitch}\n                                          size=\"sm\"\n                                          aria-label={i18n(\"notificationByChannel\", {\n                                            label: n.label,\n                                            channel: c.label.toLowerCase(),\n                                          })}\n                                          checked={!!draft.notifications[key]}\n                                          onCheckedChange={(v) =>\n                                            set(\"notifications\", { ...edits.notifications, [key]: v })\n                                          }\n                                        />\n                                      </td>\n                                    );\n                                  })}\n                                </tr>\n                              ))}\n                            </tbody>\n                          </table>\n                        </Panel>\n                      ) : (\n                        <Panel classNames={classNames}>\n                          {notificationOptions.map((n) => (\n                            <div\n                              data-slot=\"settings-notification\"\n                              key={n.id}\n                              className={cn(styles.row, classNames?.notification)}\n                            >\n                              <div\n                                data-slot=\"settings-notificationBody\"\n                                className={cn(\"min-w-0\", classNames?.notificationBody)}\n                              >\n                                <p\n                                  data-slot=\"settings-notificationLabel\"\n                                  className={cn(\"text-base\", classNames?.notificationLabel)}\n                                >\n                                  {n.label}\n                                </p>\n                                <p\n                                  data-slot=\"settings-notificationDescription\"\n                                  className={cn(\"text-sm text-ink-2\", classNames?.notificationDescription)}\n                                >\n                                  {n.description}\n                                </p>\n                              </div>\n                              <Switch\n                                className={classNames?.notificationSwitch}\n                                aria-label={n.label}\n                                checked={!!draft.notifications[n.id]}\n                                onCheckedChange={(v) => set(\"notifications\", { ...edits.notifications, [n.id]: v })}\n                              />\n                            </div>\n                          ))}\n                        </Panel>\n                      )}\n                    </Section>\n                  )}\n\n                  {section === \"api\" && (\n                    <Section\n                      classNames={classNames}\n                      title={i18n(\"apiKeys\")}\n                      description={i18n(\"apiDescription\")}\n                      action={\n                        onCreateKey && (\n                          <Dialog open={keyOpen} onOpenChange={changeKeyOpen}>\n                            <DialogTrigger asChild>\n                              <Button className={classNames?.button} size=\"sm\">\n                                <Plus\n                                  data-slot=\"settings-keyCreateIcon\"\n                                  className={cn(\"size-4\", classNames?.keyCreateIcon)}\n                                />\n                                {i18n(\"createKey\")}\n                              </Button>\n                            </DialogTrigger>\n                            <DialogContent\n                              className={classNames?.dialog}\n                              title={secret ? i18n(\"keyReady\", { name: secret.name }) : i18n(\"createKeyTitle\")}\n                              description={secret ? i18n(\"keyReadyDescription\") : i18n(\"createKeyDescription\")}\n                              dismissible={!creating}\n                              footer={\n                                secret ? (\n                                  <Button className={classNames?.button} onClick={() => changeKeyOpen(false)}>\n                                    {i18n(\"done\")}\n                                  </Button>\n                                ) : (\n                                  <>\n                                    <Button\n                                      className={classNames?.button}\n                                      variant=\"ghost\"\n                                      onClick={() => changeKeyOpen(false)}\n                                      disabled={creating}\n                                    >\n                                      {i18n(\"cancel\")}\n                                    </Button>\n                                    <Button className={classNames?.button} loading={creating} onClick={createKey}>\n                                      {i18n(\"createKey\")}\n                                    </Button>\n                                  </>\n                                )\n                              }\n                            >\n                              {secret ? (\n                                <div\n                                  data-slot=\"settings-secretBody\"\n                                  className={cn(\"grid gap-3\", classNames?.secretBody)}\n                                >\n                                  <div\n                                    data-slot=\"settings-secretRow\"\n                                    className={cn(\n                                      \"m-inset flex items-center gap-2 rounded-control px-3 py-2\",\n                                      classNames?.secretRow,\n                                    )}\n                                  >\n                                    <code\n                                      data-slot=\"settings-secret\"\n                                      className={cn(\"text-code min-w-0 flex-1 truncate text-ink\", classNames?.secret)}\n                                    >\n                                      {secret.value}\n                                    </code>\n                                    <CopyButton\n                                      className={classNames?.copyKey}\n                                      value={secret.value}\n                                      label={i18n(\"copyKey\")}\n                                    />\n                                  </div>\n                                  <Alert className={classNames?.alert} tone=\"warning\" icon={<TriangleAlert />}>\n                                    {i18n(\"keySecretWarning\")}\n                                  </Alert>\n                                </div>\n                              ) : (\n                                <form\n                                  data-slot=\"settings-keyForm\"\n                                  className={cn(\"grid gap-3\", classNames?.keyForm)}\n                                  onSubmit={(e) => {\n                                    e.preventDefault();\n                                    createKey();\n                                  }}\n                                >\n                                  <Input\n                                    className={classNames?.keyName}\n                                    label={i18n(\"keyName\")}\n                                    value={keyName}\n                                    onChange={(e) => {\n                                      setKeyName(e.target.value);\n                                      setKeyError(null);\n                                    }}\n                                    placeholder={i18n(\"keyNamePlaceholder\")}\n                                    error={keyError ?? undefined}\n                                    autoFocus\n                                  />\n                                </form>\n                              )}\n                            </DialogContent>\n                          </Dialog>\n                        )\n                      }\n                    >\n                      <Panel classNames={classNames} flush>\n                        {keys.length === 0 && (\n                          <p\n                            data-slot=\"settings-keysEmpty\"\n                            className={cn(\"px-4 py-6 text-center text-sm text-ink-2\", classNames?.keysEmpty)}\n                          >\n                            {i18n(\"keysEmpty\")}\n                          </p>\n                        )}\n                        {keys.map((k) => (\n                          <div\n                            data-slot=\"settings-key\"\n                            key={k.id}\n                            data-key={k.id}\n                            className={cn(styles.row, styles.keyRow, classNames?.key)}\n                          >\n                            <span data-slot=\"settings-keyIcon\" className={cn(styles.keyIcon, classNames?.keyIcon)}>\n                              <KeyRound className=\"size-4\" aria-hidden=\"true\" />\n                            </span>\n                            <div data-slot=\"settings-keyBody\" className={cn(\"min-w-0 flex-1\", classNames?.keyBody)}>\n                              <p\n                                data-slot=\"settings-keyTitle\"\n                                className={cn(\"truncate text-base\", classNames?.keyTitle)}\n                              >\n                                {k.name}\n                              </p>\n                              <p\n                                data-slot=\"settings-keyMeta\"\n                                className={cn(\"truncate text-xs text-ink-3\", classNames?.keyMeta)}\n                              >\n                                <code data-slot=\"settings-keyPrefix\" className={cn(\"font-mono\", classNames?.keyPrefix)}>\n                                  {k.prefix}…\n                                </code>{\" \"}\n                                <span\n                                  data-slot=\"settings-keyCreated\"\n                                  className={cn(\"font-numeric\", classNames?.keyCreated)}\n                                >\n                                  {i18n(\"keyCreated\", { date: day(k.created) })}\n                                </span>\n                              </p>\n                            </div>\n                            {onRevokeKey && (\n                              <Button\n                                className={classNames?.button}\n                                variant=\"ghost\"\n                                size=\"sm\"\n                                onClick={() => setRevoking(k)}\n                              >\n                                {i18n(\"revoke\")}\n                              </Button>\n                            )}\n                          </div>\n                        ))}\n                      </Panel>\n                      <AlertDialog\n                        open={!!revoking}\n                        onOpenChange={(o) => {\n                          if (!o && !busyRevoke) setRevoking(null);\n                        }}\n                      >\n                        {revoking && (\n                          <AlertDialogContent\n                            className={classNames?.confirmation}\n                            title={i18n(\"revokeTitle\", { name: revoking.name })}\n                            description={i18n(\"revokeDescription\")}\n                          >\n                            <AlertDialogFooter className={classNames?.confirmationFooter}>\n                              <AlertDialogCancel className={classNames?.confirmationCancel} disabled={busyRevoke}>\n                                {i18n(\"cancel\")}\n                              </AlertDialogCancel>\n                              <AlertDialogAction\n                                className={classNames?.confirmationAction}\n                                loading={busyRevoke}\n                                onClick={(e) => {\n                                  e.preventDefault();\n                                  revokeKey();\n                                }}\n                              >\n                                {i18n(\"revokeKey\")}\n                              </AlertDialogAction>\n                            </AlertDialogFooter>\n                          </AlertDialogContent>\n                        )}\n                      </AlertDialog>\n                    </Section>\n                  )}\n\n                  {section === \"danger\" && (\n                    <Section\n                      classNames={classNames}\n                      title={i18n(\"dangerZone\")}\n                      description={i18n(\"dangerDescription\", { workspace: saved.workspace ?? \"\" })}\n                    >\n                      {deletion === \"scheduled\" ? (\n                        <Alert className={classNames?.alert} tone=\"danger\" title={i18n(\"deletionScheduled\")}>\n                          {i18n(\"deletionScheduledDescription\", { workspace: saved.workspace ?? \"\" })}\n                        </Alert>\n                      ) : (\n                        <Panel classNames={classNames} tone=\"danger\">\n                          <div data-slot=\"settings-danger\" className={cn(styles.dangerRow, classNames?.danger)}>\n                            <div className=\"min-w-0\">\n                              <p className=\"text-sm text-ink\">\n                                {i18n(\"deleteWorkspaceTitle\", { workspace: saved.workspace ?? \"\" })}\n                              </p>\n                              <p className=\"text-xs text-ink-3\">{i18n(\"deleteWorkspaceDescription\")}</p>\n                            </div>\n                            <HoldToConfirm\n                              className={classNames?.delete}\n                              label={\n                                deletion === \"working\"\n                                  ? i18n(\"schedulingDeletion\")\n                                  : i18n(\"deleteAction\", { workspace: saved.workspace ?? \"\" })\n                              }\n                              confirmedLabel={i18n(\"schedulingDeletion\")}\n                              confirmed={deletion === \"working\"}\n                              disabled={deletion === \"working\"}\n                              onConfirm={deleteWorkspace}\n                            />\n                            {deleteError && (\n                              <p\n                                data-slot=\"settings-deleteError\"\n                                role=\"alert\"\n                                className={cn(\"basis-full text-sm text-red\", classNames?.deleteError)}\n                              >\n                                {deleteError}\n                              </p>\n                            )}\n                          </div>\n                        </Panel>\n                      )}\n                    </Section>\n                  )}\n                </div>\n              </FlatPanels.Provider>\n            </SavedCaption.Provider>\n          </div>\n\n          <span\n            data-slot=\"settings-announcement\"\n            className={cn(\"sr-only\", classNames?.announcement)}\n            role=\"status\"\n            aria-live=\"polite\"\n          >\n            {announcement}\n          </span>\n          {(changed.length > 0 || showSaved) && (\n            <div\n              data-slot=\"settings-saveRegion\"\n              role=\"region\"\n              aria-label={changed.length ? i18n(\"unsavedChanges\") : i18n(\"saveResult\")}\n              className={cn(styles.saveBar, classNames?.saveRegion)}\n            >\n              <div\n                ref={saveBarRef}\n                data-slot=\"settings-save-bar\"\n                data-state={barState}\n                className={cn(\n                  // it rises into place with the first unsaved change and stays put while its words change\n                  \"m-overlay pointer-events-auto flex w-full animate-enter-rise flex-wrap items-center gap-x-3 gap-y-2 rounded-panel px-4 py-3\",\n                  styles.bar,\n                  classNames?.saveBar,\n                )}\n              >\n                <span aria-hidden=\"true\" data-slot=\"settings-saveLine\" className={styles.saveLine} />\n                {/* one mark that changes with the bar's state: a dot for unsaved work, a ring while saving, a tick, a\n                    warning; keyed so each new state arrives instead of switching */}\n                <span\n                  key={barState}\n                  aria-hidden=\"true\"\n                  data-slot=\"settings-saveIcon\"\n                  className={cn(\"enter-scale\", styles.saveIcon, classNames?.saveIcon)}\n                >\n                  {barState === \"saved\" ? (\n                    <Check className=\"size-3.5\" />\n                  ) : barState === \"failed\" ? (\n                    <TriangleAlert className=\"size-3.5\" />\n                  ) : null}\n                </span>\n                <p\n                  data-slot=\"settings-saveDescription\"\n                  className={cn(\"me-auto text-sm text-ink-2\", classNames?.saveDescription)}\n                >\n                  {saveError ? (\n                    <span data-slot=\"settings-saveError\" className={cn(\"text-red\", classNames?.saveError)} role=\"alert\">\n                      {saveError.field ? i18n(\"fieldSaveFailed\") : saveError.message}\n                    </span>\n                  ) : saving ? (\n                    <TextMorph>{elapsed > 2 ? i18n(\"savingElapsed\", { elapsed }) : i18n(\"saving\")}</TextMorph>\n                  ) : showSaved && !changed.length ? (\n                    <span data-slot=\"settings-saved\" className={cn(\"text-green\", classNames?.saved)}>\n                      <TextMorph>{i18n(\"saved\")}</TextMorph>\n                    </span>\n                  ) : (\n                    <>\n                      <TextMorph>{i18n(\"unsavedCount\", { count: changed.length })}</TextMorph>\n                      {invalid && (\n                        <span data-slot=\"settings-invalid\" className={cn(\"text-red\", classNames?.invalid)}>\n                          {\" \"}\n                          {i18n(\"fixInvalidField\")}\n                        </span>\n                      )}\n                    </>\n                  )}\n                </p>\n                {saveError?.field && fieldSection(saveError.field) !== section && (\n                  <Button className={classNames?.button} variant=\"ghost\" size=\"sm\" onClick={goToField}>\n                    {i18n(\"goToField\")}\n                  </Button>\n                )}\n                {changed.length > 0 && (\n                  <>\n                    <Button\n                      className={classNames?.button}\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      onClick={discard}\n                      disabled={saving}\n                    >\n                      {i18n(\"discard\")}\n                    </Button>\n                    <Button\n                      className={classNames?.button}\n                      data-action=\"save-settings\"\n                      size=\"sm\"\n                      loading={saving}\n                      disabled={invalid}\n                      onClick={save}\n                    >\n                      {saveError ? i18n(\"tryAgain\") : i18n(\"save\")}\n                    </Button>\n                  </>\n                )}\n              </div>\n            </div>\n          )}\n        </div>\n      </div>\n      <ToastStack\n        className={classNames?.toasts}\n        style={saveBarVisible ? { bottom: `calc(${toastBottom}px + var(--pui-space-3))` } : undefined}\n        contained\n        position=\"bottom-right\"\n      />\n    </div>\n  );\n}\n\n/** \"Saved 20:41\" after a save this session, shown in the current section's header (O4 §3.2). */\nconst SavedCaption = createContext<ReactNode>(null);\n\nfunction Section({\n  classNames,\n  title,\n  description,\n  action,\n  children,\n}: {\n  title: string;\n  classNames?: SettingsClassNames;\n  description: string;\n  action?: ReactNode;\n  children: ReactNode;\n}) {\n  const id = useId();\n  const caption = useContext(SavedCaption);\n  return (\n    <section\n      data-slot=\"settings-section\"\n      className={cn(\"enter-rise flex flex-col gap-5\", classNames?.section)}\n      aria-labelledby={id}\n    >\n      <header\n        data-slot=\"settings-sectionHeader\"\n        className={cn(\"flex flex-wrap items-end gap-4 border-b border-line-soft pb-4\", classNames?.sectionHeader)}\n      >\n        <div data-slot=\"settings-sectionIdentity\" className={cn(\"me-auto min-w-0\", classNames?.sectionIdentity)}>\n          <h2\n            id={id}\n            tabIndex={-1}\n            data-slot=\"settings-section-heading\"\n            className={cn(\"text-heading\", styles.focusTarget, classNames?.sectionHeading)}\n          >\n            {title}\n          </h2>\n          <p\n            data-slot=\"settings-sectionDescription\"\n            className={cn(\"mt-0.5 text-sm text-ink-2\", classNames?.sectionDescription)}\n          >\n            {description}\n          </p>\n        </div>\n        {/* the last save's time sits in the header it belongs to, so it never pushes the form down */}\n        {caption}\n        {action}\n      </header>\n      {children}\n    </section>\n  );\n}\n\n/** Inside your own page (navigation none) a group has no surface of its own: the form sits flush on your page. */\nconst FlatPanels = createContext(false);\n\n/** One group of a section on its own surface: an optional title row, then its fields or rows. */\nfunction Panel({\n  classNames,\n  title,\n  description,\n  flush,\n  tone,\n  className,\n  children,\n}: {\n  classNames?: SettingsClassNames;\n  title?: string;\n  description?: string;\n  /** Rows and tables run to the panel's edges. */\n  flush?: boolean;\n  tone?: \"danger\";\n  className?: string;\n  children: ReactNode;\n}) {\n  const flat = useContext(FlatPanels);\n  const id = useId();\n  return (\n    <div\n      role={title ? \"group\" : undefined}\n      aria-labelledby={title ? id : undefined}\n      data-slot=\"settings-panel\"\n      data-tone={tone}\n      data-flush={flush || undefined}\n      data-flat={flat || undefined}\n      className={cn(\"@container\", !flat && \"m-surface rounded-card\", styles.panel, classNames?.panel, className)}\n    >\n      {title && (\n        <div data-slot=\"settings-panelHeader\" className={cn(styles.panelHeader, classNames?.panelHeader)}>\n          <h3 id={id} data-slot=\"settings-panelTitle\" className={cn(\"text-label text-ink\", classNames?.panelTitle)}>\n            {title}\n          </h3>\n          {description && (\n            <p data-slot=\"settings-panelDescription\" className={cn(\"text-xs text-ink-3\", classNames?.panelDescription)}>\n              {description}\n            </p>\n          )}\n        </div>\n      )}\n      {children}\n    </div>\n  );\n}\n\n/** A small window in the scheme a card offers: system shows both halves. Drawn from the theme's own tokens. */\nfunction SchemePreview({ scheme, className }: { scheme: string; className?: string }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-slot=\"settings-preview\"\n      data-scheme={scheme}\n      className={cn(styles.preview, className)}\n    >\n      <span className={styles.previewRail} />\n      <span className={styles.previewBody}>\n        <span className={styles.previewLine} />\n        <span className={styles.previewLine} />\n        <span className={styles.previewAccent} />\n      </span>\n    </span>\n  );\n}\n\nexport default Settings;\n"},{"path":"beamline/components/_shared/chip.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/chip.tsx","content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { focusAfterRemoval } from \"../../lib/focus\";\nimport { IconAction } from \"./icon-action\";\n\n/** A value inside a field (a tag, a chosen option) with its remove button. */\nexport function FieldChip({\n  children,\n  onRemove,\n  removeLabel,\n  active,\n  disabled,\n  invalid,\n  className,\n  textClassName,\n  removeClassName,\n  flip,\n  fit,\n}: {\n  children: ReactNode;\n  onRemove?: () => void;\n  removeLabel: string;\n  active?: boolean;\n  disabled?: boolean;\n  invalid?: boolean;\n  className?: string;\n  textClassName?: string;\n  removeClassName?: string;\n  /** A stable key: the chip grows in, slides and leaves a ghost inside a container that runs useFlip. */\n  flip?: string;\n  /** One of a row's items that useFit counts (the row folds what does not fit into \"+N\"). */\n  fit?: boolean;\n}) {\n  return (\n    <span\n      data-slot=\"field-chip\"\n      data-flip={flip}\n      data-fit-item={fit ? \"\" : undefined}\n      data-active={active ? \"\" : undefined}\n      className={cn(\n        \"inline-flex h-6 max-w-full items-center gap-1 rounded-chip bg-hover-2 ps-2 text-xs text-ink\",\n        !onRemove && \"pe-2\",\n        active && \"shadow-[inset_0_0_0_1px_var(--pui-accent)]\",\n        // organism-allow: tone-map-copy -- the field's invalid state, not a tone table\n        invalid && \"bg-red-tint text-red\",\n        className,\n      )}\n    >\n      <span data-slot=\"field-chip-text\" className={cn(\"truncate\", textClassName)}>\n        {children}\n      </span>\n      {onRemove && (\n        <IconAction\n          data-slot=\"field-chip-remove\"\n          size=\"xs\"\n          tabIndex={-1}\n          disabled={disabled}\n          aria-label={removeLabel}\n          onClick={(e) => {\n            e.stopPropagation();\n            const chip = e.currentTarget.closest(\"[data-slot=field-chip]\");\n            focusAfterRemoval(chip, { scope: chip?.closest(\"[data-slot=field-control]\") });\n            onRemove();\n          }}\n          className={removeClassName}\n        >\n          <X className=\"size-3\" />\n        </IconAction>\n      )}\n    </span>\n  );\n}\n"},{"path":"beamline/components/_shared/delta-ghost.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/delta-ghost.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* The ghost spans the two real values. A loss sits under the fill in a pale wash of its colour, holds for a flash and\n     drains toward the new end; a gain sits over the fill, lit, and cools. Reduced motion: shown still, then gone. */\n  .ghost {\n    --g-color: var(--pui-ink-3);\n    position: absolute;\n    inset-block: 0;\n    inset-inline-start: calc(var(--g-lo) * 100%);\n    width: calc((var(--g-hi) - var(--g-lo)) * 100%);\n    pointer-events: none;\n  }\n  .ghost[data-kind=\"loss\"] {\n    z-index: 0;\n    background: color-mix(in oklab, var(--g-color) 42%, transparent);\n    box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--g-color) 55%, transparent);\n    animation: drain calc(var(--pui-duration-flash) * 2.4) var(--pui-ease-standard) both;\n  }\n  .ghost[data-kind=\"gain\"] {\n    z-index: 2;\n    /* a lift of the fill's own light, not a white flash */\n    /* organism-allow-start: literal-colour -- a lift of the fill's own light (white at low alpha over any tone), not a theme colour */\n    background: light-dark(rgb(255 255 255 / 0.4), rgb(255 255 255 / 0.26));\n    box-shadow: inset 0 0 0 1px light-dark(rgb(255 255 255 / 0.55), rgb(255 255 255 / 0.3));\n    /* organism-allow-end */\n    animation: cool calc(var(--pui-duration-flash) * 2.2) var(--pui-ease-exit) both;\n  }\n  /* reduced motion: the same moment, without movement */\n  .ghost[data-reduced] {\n    /* organism-allow: literal-motion -- reduced motion collapses duration tokens to 1ms; this still-frame must stay readable for its moment */\n    animation: hold 1.1s steps(1, end) both;\n  }\n  /* the loss drains toward where the bar now ends (the start side of the ghost) */\n  @keyframes drain {\n    0%,\n    46% {\n      clip-path: inset(0 0 0 0);\n    }\n    100% {\n      clip-path: inset(0 100% 0 0);\n    }\n  }\n  .ghost[data-kind=\"loss\"]:dir(rtl) {\n    animation-name: drain-rtl;\n  }\n  @keyframes drain-rtl {\n    0%,\n    46% {\n      clip-path: inset(0 0 0 0);\n    }\n    100% {\n      clip-path: inset(0 0 0 100%);\n    }\n  }\n  @keyframes cool {\n    0%,\n    25% {\n      opacity: 1;\n    }\n    to {\n      opacity: 0;\n    }\n  }\n  @keyframes hold {\n    from {\n      opacity: 1;\n    }\n    to {\n      opacity: 0;\n    }\n  }\n}\n"},{"path":"beamline/components/_shared/delta-ghost.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/delta-ghost.tsx","content":"\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./delta-ghost.module.css\";\n\n/**\n * Magnitude memory for a bar (research/morphing-interfaces-2026-10-04, M4). When a value drops, the part it lost stays\n * as a pale ghost for a moment and then drains toward the new end; when it rises, the part it gained arrives lit and\n * cools. You see how much changed, not only the new value. The ghost always spans two real values (before and after),\n * never an invented in-between. A run of changes the same way extends one ghost; a reversal starts a new one. With\n * reduced motion the ghost is shown still for the same moment and then removed, so the information stays.\n */\nexport interface Ghost {\n  /** Fractions of the track (0–1): where the bar was and where it is now. */\n  from: number;\n  to: number;\n  /** Increments per change, to restart the CSS animation. */\n  key: number;\n}\n\nexport function useDeltaGhost(value: number | null | undefined) {\n  const before = useRef(value);\n  const [ghost, setGhost] = useState<Ghost | null>(null);\n  useLayoutEffect(() => {\n    const was = before.current;\n    before.current = value;\n    if (was == null || value == null || !Number.isFinite(was) || !Number.isFinite(value) || was === value) return;\n    setGhost((g) => {\n      const sameWay = g && Math.sign(g.to - g.from) === Math.sign(value - was) && g.to === was;\n      return { from: sameWay ? g.from : was, to: value, key: (g?.key ?? 0) + 1 };\n    });\n  }, [value]);\n  const clear = useCallback(() => setGhost(null), []);\n  return [ghost, clear] as const;\n}\n\n/** The ghost itself, absolutely placed in a relative, clipped track. `color` is the fill's colour. */\nexport function DeltaGhost({\n  ghost,\n  onDone,\n  color,\n  className,\n}: {\n  ghost: Ghost | null;\n  onDone: () => void;\n  color?: string;\n  className?: string;\n}) {\n  const reduced = useReducedMotionSafe();\n  if (!ghost) return null;\n  const lo = Math.max(0, Math.min(1, Math.min(ghost.from, ghost.to)));\n  const hi = Math.max(0, Math.min(1, Math.max(ghost.from, ghost.to)));\n  if (hi - lo < 0.002) return null;\n  return (\n    <span\n      key={ghost.key}\n      aria-hidden\n      data-slot=\"delta-ghost\"\n      data-kind={ghost.to < ghost.from ? \"loss\" : \"gain\"}\n      data-reduced={reduced ? \"\" : undefined}\n      className={cn(styles.ghost, className)}\n      style={{ \"--g-lo\": lo, \"--g-hi\": hi, ...(color ? { \"--g-color\": color } : {}) } as CSSProperties}\n      onAnimationEnd={(e) => {\n        if (e.target === e.currentTarget) onDone();\n      }}\n    />\n  );\n}\n"},{"path":"beamline/components/_shared/field.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/field.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A field: label (with an optional aside at the end of its row), the control, then one message line. The gap under\n   the control belongs to the message (--pui-field-gap), so a message that arrives or leaves takes its gap with it. */\n  /* a column, not a grid: a message (or a fold) that leaves pulls its gap away with a negative margin, which a flex\n     column honours and a grid track does not (it would stop at zero and stretch the leaving part back open) */\n  .field {\n    --pui-field-gap: var(--pui-space-1-5);\n    /* positioned: a hidden label or a count's spoken words (sr-only, absolutely placed) stay inside the field */\n    position: relative;\n    display: flex;\n    min-width: 0;\n    flex-direction: column;\n    gap: var(--pui-field-gap);\n  }\n  .labelRow {\n    display: flex;\n    min-width: 0;\n    align-items: baseline;\n    gap: var(--pui-space-2);\n  }\n  .aside {\n    margin-inline-start: auto;\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n  }\n\n  /* The message: one line at a time. A new line grows in to its own height (0fr to 1fr, no measuring) while the one it\n   replaces folds away above it, so the height glides from one to the other; a line that leaves with nothing after it\n   takes the gap with it. Arrival over --pui-motion-in, departure over --pui-motion-out; reduced motion keeps the fades. */\n  .messages {\n    display: grid;\n    margin-top: calc(var(--pui-field-gap, var(--pui-space-1-5)) * -1);\n  }\n  .line {\n    display: grid;\n    grid-template-rows: 1fr;\n    opacity: 1;\n    transition:\n      grid-template-rows var(--pui-motion-in) var(--pui-ease-default),\n      opacity var(--pui-fade-in, var(--pui-motion-in)) var(--pui-ease-default);\n  }\n  @starting-style {\n    .line {\n      grid-template-rows: 0fr;\n      opacity: 0;\n    }\n  }\n  /* the line a field is first drawn with does not grow in */\n  .line[data-initial] {\n    transition: none;\n  }\n  /* a swap is one change of size: the old line folds and the new one grows over the same duration and curve, so their\n     sum (the field's height) moves straight from one to the other, with no swell between; the old words fade first */\n  .line[data-leaving] {\n    grid-template-rows: 0fr;\n    opacity: 0;\n    transition:\n      grid-template-rows var(--pui-motion-out) var(--pui-ease-default),\n      opacity var(--pui-fade-in, var(--pui-motion-in)) var(--pui-ease-default);\n    pointer-events: none;\n  }\n  .messages:has(> .line[data-leaving]) > .line:not([data-leaving], [data-initial]) {\n    transition-duration: var(--pui-motion-out), var(--pui-fade-in, var(--pui-motion-in));\n  }\n  .clip {\n    min-height: 0;\n    overflow: clip;\n    overflow-clip-margin: var(--pui-space-1);\n  }\n  .message {\n    display: flex;\n    align-items: flex-start;\n    gap: var(--pui-space-1-5);\n    padding-top: var(--pui-field-gap, var(--pui-space-1-5));\n    font-size: var(--pui-text-xs);\n    line-height: 1.375;\n  }\n  .description {\n    display: block;\n    color: var(--pui-ink-3);\n  }\n  .error {\n    color: var(--pui-red);\n  }\n  .success {\n    color: var(--pui-green);\n  }\n  .icon {\n    flex-shrink: 0;\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n    margin-top: 1px;\n  }\n\n  /* The count in the label's row: tabular, quiet, shown with attention (.reveal) and in words near the limit. */\n  .count {\n    font-variant-numeric: tabular-nums;\n    white-space: nowrap;\n  }\n  .count[data-state=\"near\"],\n  .count[data-state=\"full\"] {\n    color: var(--pui-orange);\n  }\n  .count[data-state=\"over\"] {\n    color: var(--pui-red);\n  }\n\n  /* The status at the end of the frame: the spinner arrives scaled in from its centre, and closes into the check. */\n  .status {\n    display: inline-grid;\n    flex-shrink: 0;\n    place-items: center;\n    animation: status-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  @keyframes status-in {\n    from {\n      opacity: 0;\n      scale: calc(1 - 0.4 * var(--pui-motion-travel, 1));\n    }\n  }\n\n  /* The one frame every text-like control sits in: input, textarea, password, search, number, the select and date\n   triggers. Its paint (fill, edge, hover, focus) is the field role of the look in force (looks.css); invalid,\n   read-only and disabled are drawn here so they read the same in every look. */\n  .frame {\n    --_field-height: var(--pui-control-height-md);\n    position: relative;\n    display: flex;\n    min-width: 0; /* a grid or flex parent may make it narrower than the input's natural width */\n    align-items: center;\n    gap: var(--pui-space-2);\n    min-height: var(--_field-height);\n    padding-inline: var(--pui-space-2-5);\n    border-radius: var(--pui-field-radius, var(--pui-radius-control));\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    cursor: text;\n    transition:\n      box-shadow var(--pui-motion-out) var(--pui-ease-default),\n      background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .frame:hover:not([data-disabled]) {\n    transition-duration: 0ms;\n  }\n  .frame[data-state=\"open\"] {\n    box-shadow: var(--m-field-edge-focus);\n  }\n  .frame:focus-within {\n    transition: none;\n  }\n  .frame[data-disabled] {\n    opacity: var(--pui-opacity-disabled);\n    cursor: not-allowed;\n  }\n  .frame[data-readonly] {\n    --m-field-bg: transparent;\n  }\n  .sm {\n    --_field-height: var(--pui-control-height-sm);\n    padding-inline: var(--pui-space-2);\n  }\n  .lg {\n    --_field-height: var(--pui-control-height-lg);\n    padding-inline: var(--pui-space-3);\n    font-size: var(--pui-text-base);\n  }\n  .multiline {\n    align-items: stretch;\n    padding-block: var(--pui-space-2);\n  }\n\n  /* The control itself: borderless, it takes the frame's look. Focus shows on the frame, so the control's own ring is off\n   (the frame's ring is the visible focus indicator, which is always present when the control has focus). */\n  .control {\n    flex: 1;\n    min-width: 0;\n    height: 100%;\n    min-height: calc(var(--_field-height) - 2px);\n    border: 0;\n    outline: none;\n    background: transparent;\n    color: inherit;\n    font: inherit;\n    letter-spacing: inherit;\n    padding: 0;\n  }\n  .control:focus-visible {\n    outline: none;\n  }\n  .control::placeholder {\n    color: var(--pui-ink-3);\n    opacity: 1;\n  }\n  .control:disabled {\n    cursor: not-allowed;\n  }\n  textarea.control {\n    min-height: 0;\n    resize: none;\n    line-height: 1.5;\n    field-sizing: content;\n  }\n  .adornment {\n    display: inline-flex;\n    align-items: center;\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n  }\n  .adornment :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.frame {\n  composes: m-field nest-corner from global;\n}\n"},{"path":"beamline/components/_shared/field.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/field.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type CSSProperties,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n  type Ref,\n  type TransitionEvent,\n} from \"react\";\nimport { AlertCircle, CircleCheck } from \"lucide-react\";\nimport { Label } from \"../label/label\";\nimport { Spinner } from \"../spinner/spinner\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./field.module.css\";\n\nexport type FieldSize = \"sm\" | \"md\" | \"lg\";\nexport type FieldClassNames = Partial<\n  Record<\"root\" | \"label\" | \"frame\" | \"leading\" | \"trailing\" | \"description\" | \"error\" | \"success\" | \"aside\", string>\n>;\n\nexport interface FieldOptions {\n  /** Visible label, also the accessible name. */\n  label?: ReactNode;\n  /** Keep the label for assistive tech only (dense rows, toolbars). */\n  hideLabel?: boolean;\n  /** One line of help under the control. */\n  description?: ReactNode;\n  /** The problem and its fix; replaces the description and marks the control invalid. */\n  error?: ReactNode;\n  /** The confirmation that the value works (\"@mira is available\"): replaces the description, with a check. */\n  success?: ReactNode;\n  required?: boolean;\n  size?: FieldSize;\n}\n\n/** Ids and aria wiring for one field: the control gets these spread onto it. */\nexport function useFieldIds(\n  id?: string,\n  { description, error, success }: Pick<FieldOptions, \"description\" | \"error\" | \"success\"> = {},\n) {\n  const auto = useId();\n  const controlId = id ?? `field-${auto}`;\n  // the line under the field is one of three, and the control points at whichever is there\n  const descriptionId = (success || description) && !error ? `${controlId}-description` : undefined;\n  const errorId = error ? `${controlId}-error` : undefined;\n  return {\n    controlId,\n    descriptionId,\n    errorId,\n    aria: {\n      id: controlId,\n      \"aria-describedby\": [descriptionId, errorId].filter(Boolean).join(\" \") || undefined,\n      \"aria-invalid\": error ? true : undefined,\n      \"aria-errormessage\": errorId,\n    },\n  };\n}\n\n/** Label above (with an optional aside at the end of its row: a counter, a link), control frame, then the message. */\nexport function Field({\n  label,\n  hideLabel,\n  description,\n  error,\n  success,\n  required,\n  controlId,\n  descriptionId,\n  errorId,\n  aside,\n  className,\n  classNames,\n  style,\n  slot = \"field\",\n  requiredOnControl,\n  children,\n}: FieldOptions & {\n  controlId: string;\n  descriptionId?: string;\n  errorId?: string;\n  /** At the end of the label's row: a character count, \"Forgot password?\", \"Optional\". */\n  aside?: ReactNode;\n  className?: string;\n  classNames?: FieldClassNames;\n  style?: CSSProperties;\n  slot?: string;\n  /** The control carries `required` itself, so the label does not repeat it in the control's name. */\n  requiredOnControl?: boolean;\n  children: ReactNode;\n}) {\n  const labelNode = label != null && (\n    <Label\n      htmlFor={controlId}\n      required={required}\n      requiredOnControl={requiredOnControl}\n      className={cn(\"text-label\", hideLabel && \"sr-only\", classNames?.label)}\n    >\n      {label}\n    </Label>\n  );\n  return (\n    <div\n      data-slot={slot}\n      data-invalid={error ? \"\" : undefined}\n      style={style}\n      className={cn(styles.field, aside != null && \"reveal-host\", classNames?.root, className)}\n    >\n      {aside != null ? (\n        <div data-slot=\"field-label-row\" className={styles.labelRow}>\n          {labelNode}\n          <span data-slot=\"field-aside\" className={cn(styles.aside, classNames?.aside)}>\n            {aside}\n          </span>\n        </div>\n      ) : (\n        labelNode\n      )}\n      {children}\n      <FieldMessage\n        error={error}\n        success={success}\n        description={description}\n        errorId={errorId}\n        descriptionId={descriptionId}\n        classNames={classNames}\n      />\n    </div>\n  );\n}\n\ntype Tone = \"error\" | \"success\" | \"description\";\ninterface Line {\n  key: string;\n  tone: Tone;\n  content: ReactNode;\n}\nconst ICON = { error: AlertCircle, success: CircleCheck } as const;\n\n/**\n * The line under a field: its problem when there is one, else its confirmation, else its help. Shared by every field\n * and choice group. A change of line keeps its path: the new line grows in under the control while the old one folds\n * away above it, so what sits below glides instead of jumping, and a quick reversal turns around from where it is.\n * The leaving line is a still copy, hidden from assistive tech and inert; ids and aria point only at the line that is\n * there. It is gone when its fold ends, and by the clock as well: a line that leaves in the frame it arrived has no\n * fold to run, so no end is reported.\n */\nexport function FieldMessage({\n  error,\n  success,\n  description,\n  errorId,\n  descriptionId,\n  slot = \"field\",\n  classNames,\n}: Pick<FieldOptions, \"error\" | \"description\" | \"success\"> & {\n  errorId?: string;\n  descriptionId?: string;\n  /** The slot names' prefix: \"checkbox\" names the lines checkbox-error, checkbox-description. */\n  slot?: string;\n  classNames?: Pick<FieldClassNames, \"error\" | \"description\" | \"success\">;\n}) {\n  const tone: Tone | null = error ? \"error\" : success ? \"success\" : description ? \"description\" : null;\n  const content = error || success || description;\n  // a new text is a new line (one problem replacing another also folds and grows); rich content changes in place\n  const text = typeof content === \"string\" || typeof content === \"number\" ? String(content) : \"\";\n  const current: Line | null = tone ? { key: `${tone}\\u0000${text}`, tone, content } : null;\n  const [last, setLast] = useState<Line | null>(current);\n  const [leaving, setLeaving] = useState<Line | null>(null);\n  // the line a field is drawn with is simply there; only a change of line moves\n  const [changed, setChanged] = useState(false);\n  if ((last?.key ?? null) !== (current?.key ?? null)) {\n    // the line that was there leaves (one at a time: a line already leaving is gone at once)\n    setLeaving(last && last.key !== current?.key ? last : null);\n    setLast(current);\n    setChanged(true);\n  }\n  const box = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    if (!leaving) return;\n    // the clock is the leaving line's own longest transition, read once as it starts to leave (the fold's end event\n    // usually comes first), so a field resolves no motion tokens of its own\n    const el = box.current?.querySelector<HTMLElement>(\"[data-leaving]\");\n    const longest = el\n      ? Math.max(\n          0,\n          ...getComputedStyle(el)\n            .transitionDuration.split(\",\")\n            .map((t) => parseFloat(t) * (t.trim().endsWith(\"ms\") ? 1 : 1000) || 0),\n        )\n      : 0;\n    const id = window.setTimeout(() => setLeaving((l) => (l === leaving ? null : l)), longest + 250);\n    return () => window.clearTimeout(id);\n  }, [leaving]);\n  if (!current && !leaving) return null;\n  const shown = leaving && leaving.key === current?.key ? null : leaving;\n  const ids = { error: errorId, success: descriptionId, description: descriptionId };\n  const line = (l: Line, gone: boolean) => {\n    const Icon = l.tone === \"description\" ? null : ICON[l.tone];\n    return (\n      <div\n        key={l.key}\n        data-slot=\"field-message-line\"\n        data-leaving={gone ? \"\" : undefined}\n        data-initial={changed ? undefined : \"\"}\n        aria-hidden={gone || undefined}\n        inert={gone || undefined}\n        className={styles.line}\n        onTransitionEnd={\n          gone\n            ? (e: TransitionEvent<HTMLDivElement>) => {\n                if (e.target === e.currentTarget && e.propertyName === \"grid-template-rows\") setLeaving(null);\n              }\n            : undefined\n        }\n      >\n        <div className={styles.clip}>\n          <p\n            id={gone ? undefined : ids[l.tone]}\n            data-slot={`${slot}-${l.tone}`}\n            className={cn(styles.message, styles[l.tone], classNames?.[l.tone])}\n          >\n            {Icon && <Icon aria-hidden className={styles.icon} />}\n            {l.content}\n          </p>\n        </div>\n      </div>\n    );\n  };\n  return (\n    <div ref={box} data-slot=\"field-message\" className={styles.messages}>\n      {shown && line(shown, true)}\n      {current && line(current, false)}\n    </div>\n  );\n}\n\n/** What the end of a text field says about its work: a spinner while a value is checked, a drawn check when it works. */\nexport type FieldStatus = \"busy\" | \"success\";\n\n/** The frame a text-like control sits in; clicking anywhere on it focuses the control. */\nexport function FieldFrame({\n  size = \"md\",\n  invalid,\n  disabled,\n  readOnly,\n  multiline,\n  status,\n  statusLabel: statusLabelProp,\n  leading,\n  trailing,\n  className,\n  classNames,\n  children,\n  onPointerDown,\n  ref,\n}: {\n  size?: FieldSize;\n  invalid?: boolean;\n  disabled?: boolean;\n  readOnly?: boolean;\n  multiline?: boolean;\n  /** busy: a spinner at the end while the value is checked or saved; success: the ring closes into a check. */\n  status?: FieldStatus;\n  /** What the spinner says while busy (\"Checking\", \"Saving\"). */\n  statusLabel?: string;\n  leading?: ReactNode;\n  trailing?: ReactNode;\n  className?: string;\n  classNames?: FieldClassNames;\n  children: ReactNode;\n  onPointerDown?: (e: ReactPointerEvent<HTMLDivElement>) => void;\n  /** The frame itself, for a control that lays out by the frame's width (useWidthAs). */\n  ref?: Ref<HTMLDivElement>;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const statusLabel = statusLabelProp === undefined ? i18n(\"statusLabel\") : statusLabelProp;\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"field-control\"\n      data-invalid={invalid ? \"\" : undefined}\n      data-disabled={disabled ? \"\" : undefined}\n      data-readonly={readOnly ? \"\" : undefined}\n      data-status={status}\n      className={cn(styles.frame, styles[size], multiline && styles.multiline, classNames?.frame, className)}\n      onPointerDown={(e) => {\n        onPointerDown?.(e);\n        // a click on the frame's padding or an adornment that is not itself interactive focuses the control\n        const t = e.target as HTMLElement;\n        if (t === e.currentTarget || (t.closest(\"[data-adornment]\") && !t.closest(\"button, a, input, [role=button]\"))) {\n          e.preventDefault();\n          e.currentTarget.querySelector<HTMLElement>(\"input, textarea\")?.focus();\n        }\n      }}\n    >\n      {leading && (\n        <span\n          data-slot=\"field-adornment\"\n          data-adornment=\"leading\"\n          className={cn(styles.adornment, classNames?.leading)}\n        >\n          {leading}\n        </span>\n      )}\n      {children}\n      {status && (\n        // one ring for both: it spins while the value is checked and closes into the check when it works\n        <span data-slot=\"field-status\" data-adornment=\"status\" className={styles.status}>\n          <Spinner size=\"sm\" state={status === \"busy\" ? \"loading\" : \"success\"} label={statusLabel} />\n        </span>\n      )}\n      {trailing && (\n        <span\n          data-slot=\"field-adornment\"\n          data-adornment=\"trailing\"\n          className={cn(styles.adornment, classNames?.trailing)}\n        >\n          {trailing}\n        </span>\n      )}\n    </div>\n  );\n}\n\n/**\n * A character count for the label's row (Field `aside`): quiet (\"58/140\") and shown while the field has attention,\n * then always shown and in words as the limit nears (\"12 left\", \"Limit reached\", \"3 over\"), so it never competes with\n * the label at rest. Assistive tech hears it only when it crosses into those words, not at every key.\n */\nexport function FieldCount({\n  length,\n  max,\n  near: nearAt,\n  slot = \"field-count\",\n  className,\n}: {\n  length: number;\n  max: number;\n  /** How many left turns it into words: by default 10 % of the limit, at least 5 (characters); 1 suits a few tags. */\n  near?: number;\n  slot?: string;\n  className?: string;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const left = max - length;\n  const near = left <= (nearAt ?? Math.max(5, Math.ceil(max * 0.1)));\n  const state = left < 0 ? \"over\" : left === 0 ? \"full\" : near ? \"near\" : \"room\";\n  const words =\n    state === \"over\"\n      ? i18n(\"fieldOver\", { count: -left })\n      : state === \"full\"\n        ? i18n(\"fieldFull\")\n        : i18n(\"fieldLeft\", { count: left });\n  return (\n    <span\n      data-slot={slot}\n      data-state={state}\n      data-shown={state === \"room\" ? undefined : \"\"}\n      className={cn(\"reveal\", styles.count, className)}\n    >\n      <span aria-hidden>{state === \"room\" ? `${i18n.number(length)}/${i18n.number(max)}` : words}</span>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {state === \"room\" ? \"\" : words}\n      </span>\n    </span>\n  );\n}\n\n/** The class a native input or textarea needs inside a FieldFrame. */\nexport const fieldControlClass = styles.control;\n\n/** What a choice group (segmented control, radio cards) spreads onto its own root when it is a form field. */\nexport interface ChoiceFieldAria {\n  \"aria-labelledby\": string;\n  \"aria-describedby\"?: string;\n  \"aria-invalid\"?: true;\n}\n\n/**\n * Whether a choice control (segmented control, radio cards, chip group) is laid out as a form field, its label shown\n * above like every field's: when asked (`field`), when it has a description, error or required mark, and whenever its\n * label is not hidden. A view switch or a quick filter passes `hideLabel`: its label stays for assistive tech only.\n * (Trials t16 and t19: agents wrote <SegmentedControl label=\"Catering needed\" …> and the question never showed.)\n */\nexport function choiceIsField(\n  field: boolean | undefined,\n  label: ReactNode,\n  hideLabel: boolean | undefined,\n  marked: boolean,\n) {\n  return field ?? (marked || (label != null && label !== \"\" && !hideLabel));\n}\n\n/**\n * A choice group as a form field: the visible label above (with the required marker), the group, then its help or its\n * problem — the same frame and message every text field has. Groups that name themselves only for assistive tech\n * (a view switch in a toolbar) do not use it.\n */\nexport function ChoiceField({\n  slot,\n  label,\n  hideLabel,\n  required,\n  description,\n  error,\n  className,\n  style,\n  classNames,\n  requiredOnControl,\n  children,\n}: Omit<FieldOptions, \"size\" | \"success\"> & {\n  slot: string;\n  /** The group carries `aria-required` itself (a radiogroup), so the label does not repeat it in the group's name. */\n  requiredOnControl?: boolean;\n  className?: string;\n  style?: CSSProperties;\n  classNames?: Pick<FieldClassNames, \"label\" | \"description\" | \"error\">;\n  children: (aria: ChoiceFieldAria) => ReactNode;\n}) {\n  const auto = useId();\n  const labelId = `choice-${auto}-label`;\n  const messageId = error || description ? `choice-${auto}-message` : undefined;\n  return (\n    <div data-slot={slot} data-invalid={error ? \"\" : undefined} style={style} className={cn(styles.field, className)}>\n      <Label\n        id={labelId}\n        required={required}\n        requiredOnControl={requiredOnControl}\n        className={cn(\"text-label\", hideLabel && \"sr-only\", classNames?.label)}\n      >\n        {label}\n      </Label>\n      {children({\n        \"aria-labelledby\": labelId,\n        ...(messageId ? { \"aria-describedby\": messageId } : {}),\n        ...(error ? { \"aria-invalid\": true as const } : {}),\n      })}\n      <FieldMessage\n        error={error}\n        description={description}\n        errorId={messageId}\n        descriptionId={messageId}\n        classNames={classNames}\n      />\n    </div>\n  );\n}\n"},{"path":"beamline/components/_shared/fold-more.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/fold-more.tsx","content":"\"use client\";\n// organism-allow-file: raw-control -- the shared +N part itself, painted by its owner's look\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\n\nimport { useLayoutEffect, useRef, type PointerEventHandler } from \"react\";\nimport { Tooltip } from \"../tooltip/tooltip\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { rollText } from \"./roll\";\nimport { resolveCssEasing } from \"../../lib/css-values\";\nimport { cn } from \"../../lib/utils\";\n\n/** The most names the tooltip lists before it counts the rest. */\nconst NAMED = 8;\n/** A fold that has just appeared settles its count without a roll: the row measures the \"+N\" once it is on screen and\n *  may correct the count right after (the room did not change, so nothing should travel). */\nconst SETTLING_MS = 400;\n\n/**\n * The \"+N\" a folded row ends in (hooks/use-fit.ts): one part for every row of chips, tags, filters or sources, so a\n * fold reads and behaves the same in each. It counts what did not fit and names those items in the system tooltip on\n * hover or focus; with `onExpand` it is a button that shows them all (\"Show 3 more filters: …\" to a screen reader),\n * without it a quiet mark for a field that brings its items back on its own (a tag field on focus). When the room\n * changes, the count rolls to its new value: up as more fold, down as they come back (a fold that has just appeared\n * settles its first count in place). The row gives it its geometry through `className` (height, radius, padding), so\n * it stands like its neighbours; the look and the behaviour are the part's.\n */\nexport function FoldMore({\n  names,\n  noun,\n  onExpand,\n  onPointerDown,\n  marks = true,\n  hidden,\n  slot,\n  className,\n}: {\n  /** The folded items, in order: their count is shown; their names are the tooltip and the spoken label. */\n  names: string[];\n  /** What the items are, for the spoken label (\"sources\", \"filters\"). */\n  noun?: string;\n  /** Show every folded item (the row expands, a menu opens). Without it the mark is not a control. */\n  onExpand?: () => void;\n  onPointerDown?: PointerEventHandler<HTMLElement>;\n  /** Mark it as the row's overflow control for useFit and useFlip (`data-fit-more`, `data-flip=\"more\"`). Off when a\n   *  wrapper of the caller's is the row's child and carries the marks (a list item). Default true. */\n  marks?: boolean;\n  /** Hidden from assistive tech: the control it sits in already names every item (a multi-select trigger). */\n  hidden?: boolean;\n  /** The owner's part name (data-slot). */\n  slot?: string;\n  className?: string;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const count = useRef<HTMLSpanElement>(null);\n  const motion = useMotionTokens(count);\n  const last = useRef(names.length);\n  const born = useRef<number | null>(null);\n  useLayoutEffect(() => {\n    born.current ??= performance.now();\n    const was = last.current;\n    last.current = names.length;\n    const el = count.current;\n    if (!el || was === names.length || motion.reduced || performance.now() - born.current < SETTLING_MS) return;\n    rollText(el, `+${i18n.number(was)}`, `+${i18n.number(names.length)}`, names.length > was ? 1 : -1, {\n      duration: motion.duration.standard * 1000,\n      easing: resolveCssEasing(el, \"--pui-ease-default\"),\n    });\n  }, [names.length, i18n]); // eslint-disable-line react-hooks/exhaustive-deps -- rolls on a change of the count only\n  const listed =\n    names.length > NAMED\n      ? i18n(\"foldMore\", { list: i18n.list(names.slice(0, NAMED)), count: names.length - NAMED })\n      : i18n.list(names);\n  const marked = marks ? { \"data-fit-more\": \"\", \"data-flip\": \"more\" } : {};\n  const look = cn(\n    \"relative inline-flex h-6 shrink-0 items-center rounded-chip px-2 text-xs text-ink-2 tabular-nums\",\n    \"shadow-[inset_0_0_0_1px_var(--pui-line)]\",\n  );\n  const body = (\n    <span ref={count} data-slot={slot ? `${slot}-count` : undefined}>\n      +{i18n.number(names.length)}\n    </span>\n  );\n  return (\n    <Tooltip content={listed}>\n      {onExpand ? (\n        <button\n          type=\"button\"\n          data-slot={slot}\n          {...marked}\n          aria-label={i18n(\"foldShow\", { count: names.length, noun: noun ?? \"\", list: i18n.list(names) })}\n          onClick={onExpand}\n          onPointerDown={onPointerDown}\n          className={cn(look, \"ix hover:bg-hover hover:text-ink\", className)}\n        >\n          {body}\n        </button>\n      ) : (\n        <span\n          data-slot={slot}\n          {...marked}\n          aria-hidden={hidden || undefined}\n          onPointerDown={onPointerDown}\n          className={cn(look, className)}\n        >\n          {body}\n        </span>\n      )}\n    </Tooltip>\n  );\n}\n"},{"path":"beamline/components/_shared/glide.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/glide.tsx","content":"\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type RefObject } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport overlay from \"./overlay.module.css\";\n\n/**\n * The gliding highlight for a list whose active row is tracked in state (command menu, listbox): one plate travels to\n * the active row for the arrow keys and the pointer alike, over --pui-duration-fast. It appears in place (no travel)\n * the first time and whenever `reset` changes (a new page or a new query), so typing never sends it flying.\n * The list must be the plate's offset parent (position: relative); rows stay above it (overlay.item is z-index 1).\n */\nexport function GlidePlate({\n  list,\n  selector,\n  reset,\n  axis = \"y\",\n  className,\n}: {\n  list: RefObject<HTMLElement | null>;\n  /** Finds the active row inside `list`; null when nothing is active. */\n  selector: string | null;\n  reset?: unknown;\n  /** \"y\" for a list (the plate spans the width); \"xy\" for a row or grid of cells (it takes each cell's size). */\n  axis?: \"y\" | \"xy\";\n  className?: string;\n}) {\n  const [plate, setPlate] = useState<{ x: number; y: number; w: number; h: number; jump: boolean } | null>(null);\n  const lastReset = useRef(reset);\n  const self = useRef<HTMLSpanElement>(null);\n  // eslint-disable-next-line react-hooks/exhaustive-deps -- every render on purpose; setPlate bails out when unchanged\n  useLayoutEffect(() => {\n    // the list holds the plate, and React attaches a parent's ref only after its children's layout effects: on the\n    // first commit the list is the plate's own parent (it placed itself only when something rendered again later)\n    const host = list.current ?? self.current?.parentElement;\n    const row = selector ? host?.querySelector<HTMLElement>(selector) : null;\n    const fresh = lastReset.current !== reset;\n    lastReset.current = reset;\n    if (!row) {\n      setPlate((p) => (p && p.h ? { ...p, h: 0 } : p));\n      return;\n    }\n    const x = row.offsetLeft,\n      y = row.offsetTop,\n      w = row.offsetWidth,\n      h = row.offsetHeight;\n    setPlate((p) =>\n      p && p.x === x && p.y === y && p.w === w && p.h === h && !fresh ? p : { x, y, w, h, jump: fresh || !p?.h },\n    );\n    // every render: rows can arrive or change height without the selector changing; an unchanged place keeps the state\n  });\n  return (\n    <span\n      ref={self}\n      aria-hidden\n      data-slot=\"list-highlight\"\n      className={cn(overlay.glide, className)}\n      data-on={plate?.h ? \"\" : undefined}\n      data-jump={plate?.jump ? \"\" : undefined}\n      data-axis={axis}\n      style={\n        !plate\n          ? undefined\n          : axis === \"y\"\n            ? { transform: `translateY(${plate.y}px)`, height: plate.h || undefined }\n            : { transform: `translate(${plate.x}px, ${plate.y}px)`, width: plate.w, height: plate.h || undefined }\n      }\n    />\n  );\n}\n\n/**\n * A GlidePlate for lists whose highlight is the pointer or keyboard focus rather than state (time picker, plain option\n * lists): it follows the row under the pointer or holding focus, and fades when neither is in the list. Rows drop\n * their own hover and focus tint.\n */\nexport function FollowPlate({\n  list,\n  rows,\n  className,\n}: {\n  list: RefObject<HTMLElement | null>;\n  /** Matches a row inside `list`, e.g. \"[role=option]\". */\n  rows: string;\n  className?: string;\n}) {\n  const [hot, setHot] = useState<string | null>(null);\n  // after the commit, when the list's ref is attached (a layout effect here ran before it and never listened)\n  useEffect(() => {\n    const el = list.current;\n    if (!el) return;\n    let n = 0;\n    const mark = (target: EventTarget | null) => {\n      const row = (target as HTMLElement | null)?.closest?.<HTMLElement>(rows);\n      if (!row || !el.contains(row)) return;\n      row.dataset.glideKey ??= String(++n);\n      setHot(row.dataset.glideKey);\n    };\n    const over = (e: Event) => mark(e.target);\n    const leave = () => {\n      if (!el.contains(document.activeElement)) setHot(null);\n    };\n    const blur = (e: FocusEvent) => {\n      if (!el.contains(e.relatedTarget as Node)) setHot(null);\n    };\n    el.addEventListener(\"pointerover\", over);\n    el.addEventListener(\"focusin\", over);\n    el.addEventListener(\"pointerleave\", leave);\n    el.addEventListener(\"focusout\", blur);\n    return () => {\n      el.removeEventListener(\"pointerover\", over);\n      el.removeEventListener(\"focusin\", over);\n      el.removeEventListener(\"pointerleave\", leave);\n      el.removeEventListener(\"focusout\", blur);\n    };\n  }, [list, rows]);\n  return <GlidePlate list={list} selector={hot ? `[data-glide-key=\"${hot}\"]` : null} className={className} />;\n}\n"},{"path":"beamline/components/_shared/icon-action.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/icon-action.module.css","content":"@layer properties, theme, base, components, utilities;\n/* The small square action that sits inside another element: a field's clear and open buttons, a chip's remove, a\n   toast's close, a carousel's previous and next. One recipe so every one of them answers alike: quiet ink at rest,\n   the hover plate at once, the press at once, both let go over --pui-motion-out. Its corner is the chip radius: under\n   the nested-corner rule (.nest-corner) a part whose own corner is the chip never changes inside a rounded frame. */\n@layer components {\n  .root {\n    position: relative;\n    display: grid;\n    flex-shrink: 0;\n    place-items: center;\n    inline-size: var(--_size);\n    block-size: var(--_size);\n    border-radius: var(--pui-radius-chip);\n    color: var(--pui-ink-3);\n    /* opacity and translate stay in the list so a .reveal action keeps its own arrival whichever rule wins */\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      transform var(--pui-motion-out) var(--pui-ease-default),\n      opacity var(--pui-motion-out) var(--pui-ease-default),\n      translate var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root:hover:not(:disabled),\n  .root[data-pressed] {\n    background-color: var(--pui-hover);\n    color: var(--pui-ink);\n  }\n  .root:hover {\n    transition-duration: 0ms;\n  }\n  .root:active:not(:disabled) {\n    transform: scale(var(--pui-press-scale));\n    transition-duration: 0ms;\n  }\n  .root:disabled {\n    cursor: default;\n    opacity: 0.4;\n  }\n  .root[data-tone=\"ink-2\"] {\n    color: var(--pui-ink-2);\n  }\n  .root[data-tone=\"inherit\"] {\n    color: inherit;\n  }\n  .root[data-tone=\"inherit\"]:hover:not(:disabled) {\n    color: inherit;\n  }\n  /* on an inverted (ink) surface: inverse ink, a faint inverse plate */\n  .root[data-tone=\"inverse\"] {\n    color: color-mix(in oklab, var(--pui-ink-inverse) 72%, transparent);\n  }\n  .root[data-tone=\"inverse\"]:hover:not(:disabled),\n  .root[data-tone=\"inverse\"][data-pressed] {\n    background-color: color-mix(in oklab, var(--pui-ink-inverse) 12%, transparent);\n    color: var(--pui-ink-inverse);\n  }\n\n  .xs {\n    --_size: 1.25rem;\n  }\n  /* a 20px action still takes a 24px hit (WCAG 2.5.8): the bleed is outside the drawn square */\n  .xs::after {\n    position: absolute;\n    inset: calc((var(--pui-hit-min) - 1.25rem) / -2);\n    content: \"\";\n  }\n  .sm {\n    --_size: 1.5rem;\n  }\n  .md {\n    --_size: 1.75rem;\n  }\n  .lg {\n    --_size: 2rem;\n  }\n}\n"},{"path":"beamline/components/_shared/icon-action.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/icon-action.tsx","content":"\"use client\";\n// organism-allow-file: raw-control -- the shared part every element draws this control from\n\nimport type { ButtonHTMLAttributes, Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./icon-action.module.css\";\n\n/** 20 · 24 · 28 · 32 px. A 20px action keeps a 24px hit outside its square. */\nexport type IconActionSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nexport interface IconActionProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  /** The action's name: the button holds only an icon. */\n  \"aria-label\": string;\n  size?: IconActionSize;\n  /** Rest ink: ink-3 (the default, an action beside content), ink-2 (an action that is the row's point), inverse (on\n   *  an inverted ink surface) or inherit (on a painted surface that brings its own colours). */\n  tone?: \"ink-3\" | \"ink-2\" | \"inverse\" | \"inherit\";\n  /** Holds the hover plate while what it opens is open, or while it is switched on. */\n  pressed?: boolean;\n  ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * The small square icon action inside another element (clear, remove, dismiss, previous, next, retry). One recipe for\n * all of them; a full-size action with words is a Button, and Button with iconOnly is the standalone square.\n */\nexport function IconAction({\n  size = \"sm\",\n  tone = \"ink-3\",\n  pressed,\n  type = \"button\",\n  className,\n  ref,\n  ...props\n}: IconActionProps) {\n  return (\n    <button\n      ref={ref}\n      type={type}\n      data-tone={tone === \"ink-3\" ? undefined : tone}\n      data-pressed={pressed ? \"\" : undefined}\n      className={cn(styles.root, styles[size], className)}\n      {...props}\n    />\n  );\n}\n"},{"path":"beamline/components/_shared/list-controls.messages.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/list-controls.messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy for this helper only. */\nexport const english = {\n  pagination: \"Pagination\",\n  previousPage: \"Previous page\",\n  nextPage: \"Next page\",\n  pageUnknown: \"Page {page} · total unknown\",\n  rowsPerPage: \"Rows per page\",\n  perPage: \"{value} / page\",\n  loadMore: \"Load more\",\n  retryMore: \"Retry loading more\",\n  loadingMore: \"Loading more…\",\n  end: \"End of results\",\n  selectRows: \"Select rows\",\n  allVisible: \"All visible rows\",\n  selected: \"{count} selected\",\n  selectedHidden: \"{count} selected · {hidden} not shown\",\n  clearSelection: \"Clear selection\",\n  selectAllMatching: \"Select all {count} matching rows\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/_shared/list-controls.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/list-controls.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./list-controls.messages\";\nimport { english as feedbackEnglish } from \"./list-feedback.messages\";\n\nimport { useEffect, useRef, type RefObject } from \"react\";\nimport { Button } from \"../button/button\";\nimport { Pagination } from \"../pagination/pagination\";\nimport { DropdownMenu } from \"../dropdown-menu/dropdown-menu\";\nimport { Select } from \"../select/select\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport type { ListWindowProps } from \"../../lib/list\";\nimport { useListEnd } from \"./list-state\";\n\nexport function ListPagination({\n  page,\n  pageSize,\n  rowCount,\n  onPageChange,\n  onPageSizeChange,\n  hasMore,\n}: ListWindowProps & { page: number; pageSize: number; onPageChange: (page: number) => void }) {\n  const i18n = useMessages(\"_shared-list-controls\", english);\n  return (\n    <div data-slot=\"data-grid-pagination\" className=\"flex min-w-0 flex-wrap items-center gap-3 p-3\">\n      <div className=\"min-w-0 flex-1 basis-64\">\n        {rowCount != null ? (\n          <Pagination\n            page={page}\n            pageSize={pageSize}\n            rowCount={rowCount}\n            pageCount={Math.max(1, Math.ceil(rowCount / pageSize))}\n            onPageChange={onPageChange}\n          />\n        ) : (\n          <nav aria-label={i18n(\"pagination\")} className=\"flex items-center gap-3\">\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              aria-label={i18n(\"previousPage\")}\n              disabled={page <= 1}\n              onClick={() => onPageChange(page - 1)}\n            >\n              <ChevronLeft className=\"rtl:rotate-180\" />\n            </Button>\n            <span className=\"text-xs text-ink-2\">{i18n(\"pageUnknown\", { page })}</span>\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              aria-label={i18n(\"nextPage\")}\n              disabled={hasMore === false}\n              onClick={() => onPageChange(page + 1)}\n            >\n              <ChevronRight className=\"rtl:rotate-180\" />\n            </Button>\n          </nav>\n        )}\n      </div>\n      {onPageSizeChange && (\n        <Select\n          label={i18n(\"rowsPerPage\")}\n          hideLabel\n          value={String(pageSize)}\n          onValueChange={(value) => onPageSizeChange(Number(value))}\n          options={[...new Set([10, 25, 50, 100, pageSize])]\n            .sort((a, b) => a - b)\n            .map((value) => ({ value: String(value), label: i18n(\"perPage\", { value }) }))}\n        />\n      )}\n    </div>\n  );\n}\n\nexport function ListEnd({\n  root,\n  resultKey,\n  hasMore = true,\n  automatic = true,\n  loadLabel: loadLabelProp,\n  ...props\n}: ListWindowProps & {\n  loading?: boolean;\n  error?: unknown;\n  root?: RefObject<HTMLElement | null>;\n  resultKey: string | number;\n  automatic?: boolean;\n  loadLabel?: string;\n}) {\n  const i18n = useMessages(\"_shared-list-controls\", english);\n  const loadLabel = loadLabelProp === undefined ? i18n(\"loadMore\") : loadLabelProp;\n  const end = useRef<HTMLDivElement>(null);\n  const { request, failure, busy } = useListEnd({\n    ...props,\n    hasMore,\n    resultKey,\n  });\n  useEffect(() => {\n    if (!automatic || !end.current || !props.onEndReached || !hasMore || typeof IntersectionObserver === \"undefined\")\n      return;\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting) void request();\n      },\n      { root: root?.current ?? null },\n    );\n    observer.observe(end.current);\n    return () => observer.disconnect();\n  }, [root, request, props.onEndReached, hasMore, automatic]);\n  if (!props.onEndReached) return null;\n  return (\n    <div\n      ref={end}\n      data-slot=\"data-grid-end\"\n      className=\"flex min-w-0 flex-wrap items-center justify-center gap-2 p-3\"\n      aria-live=\"polite\"\n    >\n      {failure && (\n        <span role=\"status\" className=\"text-xs text-red\">\n          {failure}\n        </span>\n      )}\n      {hasMore ? (\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          loading={props.loadingMore || busy}\n          disabled={props.loading}\n          onClick={() => void request(true)}\n        >\n          {failure || props.error ? i18n(\"retryMore\") : props.loadingMore || busy ? i18n(\"loadingMore\") : loadLabel}\n        </Button>\n      ) : (\n        <span className=\"text-xs text-ink-3\">{i18n(\"end\")}</span>\n      )}\n    </div>\n  );\n}\n\n/** Inline status leaves the list and its controls mounted during a refresh or a failure. */\nexport function ListFeedback({ loading, error, onRetry }: import(\"../../lib/list\").ListProps) {\n  const i18n = useMessages(\"_shared-list-feedback\", feedbackEnglish);\n  const message = error instanceof Error ? error.message : error;\n  if (!message && !loading) return null;\n  return (\n    <div role=\"status\" className=\"flex flex-wrap items-center gap-2 p-3 text-sm text-ink-2\">\n      {message || i18n(\"loading\")}\n      {message && onRetry && (\n        <Button size=\"sm\" variant=\"secondary\" onClick={() => void onRetry()}>\n          {i18n(\"tryAgain\")}\n        </Button>\n      )}\n    </div>\n  );\n}\n\nexport function ListFooter({\n  state,\n  props,\n  automatic = true,\n  loadLabel,\n}: {\n  state: {\n    page: number;\n    pageSize: number;\n    rowCount?: number | null;\n    pagination: boolean;\n    setPage: (value: number) => void;\n    setPageSize: (value: number) => void;\n    query: string;\n    filters: import(\"../../lib/list\").ListFilters;\n    sort: import(\"../../lib/list\").ListSort;\n    rows: unknown[];\n  };\n  props: import(\"../../lib/list\").ListProps;\n  automatic?: boolean;\n  loadLabel?: string;\n}) {\n  return (\n    <>\n      {state.pagination && (\n        <ListPagination\n          page={state.page}\n          pageSize={state.pageSize}\n          rowCount={state.rowCount}\n          hasMore={props.hasMore}\n          onPageChange={state.setPage}\n          onPageSizeChange={state.setPageSize}\n        />\n      )}\n      <ListEnd\n        {...props}\n        automatic={automatic}\n        loadLabel={loadLabel}\n        resultKey={JSON.stringify([\n          state.page,\n          state.rows.map((row) => (row as { id?: string; symbol?: string }).id ?? (row as { symbol?: string }).symbol),\n          state.query,\n          state.filters,\n          state.sort,\n        ])}\n      />\n    </>\n  );\n}\n\n/** Custom lists expose selection without replacing their existing row-opening action. */\nexport function ListSelection<T>({\n  state,\n  props,\n  getRowId,\n  getLabel,\n}: {\n  state: {\n    rows: T[];\n    selected: string[];\n    setSelected: (ids: string[]) => void;\n    rowCount?: number | null;\n    query: string;\n    filters: import(\"../../lib/list\").ListFilters;\n  };\n  props: import(\"../../lib/list\").ListProps;\n  getRowId: (row: T) => string;\n  getLabel: (row: T) => string;\n}) {\n  const i18n = useMessages(\"_shared-list-controls\", english);\n  if (!props.selectable && !state.selected.length) return null;\n  const ids = state.rows.map(getRowId);\n  const hidden = state.selected.filter((id) => !ids.includes(id));\n  const all = ids.length > 0 && ids.every((id) => state.selected.includes(id));\n  return (\n    <div data-slot=\"list-selection\" className=\"flex flex-wrap items-center gap-2 p-2\">\n      {props.selectable && (\n        <DropdownMenu\n          label={i18n(\"selectRows\")}\n          items={[\n            {\n              id: \"all\",\n              label: i18n(\"allVisible\"),\n              checked: all,\n              disabled: !ids.length,\n              onSelect: () => state.setSelected(all ? hidden : [...new Set([...state.selected, ...ids])]),\n            },\n            ...state.rows.map((row) => {\n              const id = getRowId(row);\n              return {\n                id,\n                label: getLabel(row),\n                checked: state.selected.includes(id),\n                onSelect: () =>\n                  state.setSelected(\n                    state.selected.includes(id) ? state.selected.filter((key) => key !== id) : [...state.selected, id],\n                  ),\n              };\n            }),\n          ]}\n        />\n      )}\n      {state.selected.length > 0 && (\n        <>\n          <span role=\"status\" className=\"text-xs text-ink-2\">\n            {i18n(hidden.length ? \"selectedHidden\" : \"selected\", {\n              count: state.selected.length,\n              hidden: hidden.length,\n            })}\n          </span>\n          <Button size=\"sm\" variant=\"ghost\" onClick={() => state.setSelected([])}>\n            {i18n(\"clearSelection\")}\n          </Button>\n        </>\n      )}\n      {props.manual &&\n        props.allowSelectAllMatching &&\n        props.onSelectAllMatching &&\n        all &&\n        state.rowCount != null &&\n        state.rowCount > ids.length && (\n          <Button\n            size=\"sm\"\n            variant=\"ghost\"\n            onClick={() =>\n              props.onSelectAllMatching?.({ rowCount: state.rowCount!, query: state.query, filters: state.filters })\n            }\n          >\n            {i18n(\"selectAllMatching\", { count: state.rowCount })}\n          </Button>\n        )}\n    </div>\n  );\n}\n"},{"path":"beamline/components/_shared/list-feedback.messages.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/list-feedback.messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy for this helper only. */\nexport const english = {\n  loading: \"Loading…\",\n  tryAgain: \"Try again\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/_shared/list-state.messages.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/list-state.messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy for this helper only. */\nexport const english = {\n  moreRowsDidNotLoad: \"More rows did not load\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/_shared/list-state.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/list-state.ts","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./list-state.messages\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\nimport type { ListWindowProps } from \"../../lib/list\";\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\nimport { useControllable } from \"../../hooks/use-controllable\";\n\nexport function hasPagination(props: ListWindowProps) {\n  return (\n    props.pagination ??\n    (!props.onEndReached &&\n      [props.page, props.defaultPage, props.pageSize, props.defaultPageSize, props.rowCount, props.total].some(\n        (v) => v !== undefined,\n      ))\n  );\n}\n\nexport function useListWindow(props: ListWindowProps) {\n  useDeprecatedProp(\"List\", \"total\", \"rowCount\", props.total !== undefined);\n  const [rawPage, changePage] = useControllable(props.page, props.defaultPage ?? 1, props.onPageChange);\n  const [rawSize, changeSize] = useControllable(props.pageSize, props.defaultPageSize ?? 25, props.onPageSizeChange);\n  const page = Math.max(1, Math.floor(rawPage) || 1);\n  const pageSize = Math.max(1, Math.floor(rawSize) || 25);\n  const setPage = useCallback((next: number) => changePage(Math.max(1, Math.floor(next) || 1)), [changePage]);\n  const resetPage = useCallback(() => {\n    if (page !== 1) setPage(1);\n  }, [page, setPage]);\n  const setPageSize = useCallback(\n    (next: number) => {\n      changeSize(Math.max(1, Math.floor(next) || 25));\n      resetPage();\n    },\n    [changeSize, resetPage],\n  );\n  return {\n    rowCount: props.rowCount !== undefined ? props.rowCount : props.total,\n    page,\n    pageSize,\n    setPage,\n    setPageSize,\n    resetPage,\n    pagination: hasPagination(props),\n  };\n}\n\nexport { useRetainedRows } from \"./retained-rows\";\n\n/** Detect a result boundary once. A failed callback stops automatic retries until explicitly retried. */\nexport function useListEnd({\n  onEndReached,\n  hasMore,\n  loadingMore,\n  loading,\n  error,\n  resultKey,\n}: ListWindowProps & { loading?: boolean; error?: unknown; resultKey: string | number }) {\n  const i18n = useMessages(\"_shared-list-state\", english);\n  const [failed, setFailed] = useState<{ key: string | number; message: string | null } | null>(null);\n  const failure = failed?.key === resultKey ? (failed.message ?? i18n(\"moreRowsDidNotLoad\")) : null;\n  const [busy, setBusy] = useState(false);\n  const requested = useRef<string | number | null>(null);\n  const inFlight = useRef(false);\n  const request = useCallback(\n    async (retry = false) => {\n      if (\n        !onEndReached ||\n        !hasMore ||\n        loading ||\n        loadingMore ||\n        inFlight.current ||\n        (!retry && (error || failure || requested.current === resultKey))\n      )\n        return;\n      requested.current = resultKey;\n      inFlight.current = true;\n      setBusy(true);\n      setFailed(null);\n      try {\n        await onEndReached();\n      } catch (e) {\n        setFailed({ key: resultKey, message: e instanceof Error ? e.message : null });\n      } finally {\n        inFlight.current = false;\n        setBusy(false);\n      }\n    },\n    [onEndReached, hasMore, loading, loadingMore, error, failure, resultKey],\n  );\n  return { request, failure, busy };\n}\n"},{"path":"beamline/components/_shared/listbox.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/listbox.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\n\nimport { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { Highlight, filterRanked } from \"./match\";\nimport { cn } from \"../../lib/utils\";\nimport { GlidePlate } from \"./glide\";\nimport overlay from \"./overlay.module.css\";\n\nexport interface ListOption {\n  value: string;\n  label: string;\n  description?: string;\n  /** Other words that should find it. */\n  keywords?: string[];\n  icon?: ReactNode;\n  disabled?: boolean;\n  /** Heading it sits under (Billing address). With no query the list shows a heading where the group changes, in the\n   * given order; while searching, results rank flat and each row names its group beside its description. */\n  group?: string;\n}\n\n/** Lists longer than this mount only the rows near the visible window. */\nconst WINDOW = 200;\n/** A one-line row's height before it is measured, and rows kept mounted past each edge. */\nconst ROW = 34;\nconst OVERSCAN = 8;\n\n/** Filtering and the active row for a search-as-you-type list; the field keeps focus and points at the active row. */\nexport function useListbox(options: ListOption[], query: string, extra?: ListOption | null) {\n  // a group's name finds its options too; `extra` (a row that acts, such as Create \"…\") always comes last\n  const ranked = useMemo(\n    () => filterRanked(options, query, (o) => (o.group ? { ...o, keywords: [...(o.keywords ?? []), o.group] } : o)),\n    [options, query],\n  );\n  const filtered = useMemo(() => (extra ? [...ranked, extra] : ranked), [ranked, extra]);\n  const [active, setActive] = useState(0);\n  const viaKeys = useRef(false);\n  useEffect(() => {\n    setActive(\n      Math.max(\n        0,\n        filtered.findIndex((o) => !o.disabled),\n      ),\n    );\n  }, [query]); // eslint-disable-line react-hooks/exhaustive-deps -- reset per question\n  const move = (delta: number) => {\n    if (!filtered.length) return;\n    viaKeys.current = true;\n    let i = active;\n    for (let n = 0; n < filtered.length; n++) {\n      i = (i + delta + filtered.length) % filtered.length;\n      if (!filtered[i].disabled) break;\n    }\n    setActive(i);\n  };\n  // Home, End and Page keys: to a place in the whole list (no wrap), the nearest enabled row from there\n  const jump = (to: number) => {\n    if (!filtered.length) return;\n    viaKeys.current = true;\n    const at = Math.max(0, Math.min(filtered.length - 1, to));\n    const back = to < active;\n    for (let n = 0; n < filtered.length; n++) {\n      const i = back ? at + n : at - n;\n      if (i >= 0 && i < filtered.length && !filtered[i].disabled) return setActive(i);\n    }\n  };\n  return {\n    filtered,\n    active: Math.min(active, filtered.length - 1),\n    setActive,\n    move,\n    jump,\n    viaKeys,\n    current: filtered[Math.min(active, filtered.length - 1)],\n  };\n}\n\n/** The list itself: rows in the system's menu look, the match emphasised, chosen rows checked. */\nexport function Listbox({\n  id,\n  label,\n  options,\n  query,\n  active,\n  onActive,\n  isSelected,\n  onPick,\n  multiple,\n  emptyText,\n  viaKeys,\n  className,\n}: {\n  id: string;\n  label: string;\n  options: ListOption[];\n  query: string;\n  active: number;\n  onActive: (i: number) => void;\n  isSelected: (value: string) => boolean;\n  onPick: (option: ListOption) => void;\n  multiple?: boolean;\n  emptyText?: ReactNode;\n  viaKeys: { current: boolean };\n  className?: string;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const list = useRef<HTMLDivElement>(null);\n  // a long list mounts only the rows near the visible window; spacers above and below keep its scroll height true\n  const windowed = options.length > WINDOW;\n  const heights = useRef(new Map<string, number>());\n  const [scroll, setScroll] = useState({ top: 0, height: 300 });\n  const [measured, setMeasured] = useState(0);\n  const offsets = useMemo(() => {\n    if (!windowed) return null;\n    const out = new Float64Array(options.length + 1);\n    for (let i = 0; i < options.length; i++)\n      out[i + 1] = out[i] + (heights.current.get(options[i].value) ?? (options[i].description ? 46 : ROW));\n    return out;\n  }, [windowed, options, measured]); // eslint-disable-line react-hooks/exhaustive-deps -- measured invalidates the cached heights\n  // the first row whose bottom is below y\n  const rowAt = (y: number) => {\n    let lo = 0;\n    let hi = options.length;\n    while (lo < hi) {\n      const mid = (lo + hi) >> 1;\n      if (offsets![mid + 1] <= y) lo = mid + 1;\n      else hi = mid;\n    }\n    return lo;\n  };\n  const first = windowed ? Math.max(0, rowAt(scroll.top) - OVERSCAN) : 0;\n  const last = windowed ? Math.min(options.length, rowAt(scroll.top + scroll.height) + OVERSCAN + 1) : options.length;\n  // the mounted rows measure once each time the window moves; a row only re-renders when a height changed\n  useLayoutEffect(() => {\n    if (!windowed || !list.current) return;\n    let changed = false;\n    for (const el of list.current.querySelectorAll<HTMLElement>(\"[data-row]\")) {\n      const h = el.offsetHeight;\n      if (h && heights.current.get(el.dataset.row!) !== h) {\n        heights.current.set(el.dataset.row!, h);\n        changed = true;\n      }\n    }\n    if (changed) setMeasured((m) => m + 1);\n  }, [windowed, first, last, options]);\n  useEffect(() => {\n    if (!viaKeys.current) return;\n    const row = list.current?.querySelector(`#${CSS.escape(`${id}-${active}`)}`);\n    if (row) row.scrollIntoView({ block: \"nearest\" });\n    else if (offsets && list.current) {\n      // not mounted: scroll its place into view and the row renders there\n      const el = list.current;\n      if (offsets[active] < el.scrollTop) el.scrollTop = offsets[active];\n      else if (offsets[active + 1] > el.scrollTop + el.clientHeight)\n        el.scrollTop = offsets[active + 1] - el.clientHeight;\n    }\n  }, [active, id, viaKeys, offsets]);\n  // what a search found, said once it settles (a polite status beside the list, never inside it: a listbox holds\n  // options only), so a screen reader hears \"3 results\" or that nothing matched while focus stays in the field\n  const found = !query.trim()\n    ? \"\"\n    : options.length\n      ? i18n(\"results\", { count: options.length })\n      : typeof emptyText === \"string\"\n        ? emptyText\n        : i18n(\"noMatch\", { query });\n  return (\n    <>\n      <span role=\"status\" className=\"sr-only\" data-slot=\"listbox-status\">\n        {found}\n      </span>\n      <div\n        ref={list}\n        id={id}\n        role=\"listbox\"\n        aria-label={label}\n        aria-multiselectable={multiple || undefined}\n        onPointerMove={() => {\n          viaKeys.current = false;\n        }}\n        onScroll={\n          windowed\n            ? (e) => setScroll({ top: e.currentTarget.scrollTop, height: e.currentTarget.clientHeight })\n            : undefined\n        }\n        className={cn(\n          \"relative max-h-[min(300px,var(--radix-popover-content-available-height,300px))] overflow-y-auto p-1\",\n          className,\n        )}\n      >\n        <GlidePlate list={list} selector={options.length ? '[role=\"option\"][data-active]' : null} reset={query} />\n        {options.length === 0 && (\n          <div className=\"px-3 py-6 text-center text-sm text-ink-3\">{emptyText ?? i18n(\"noMatch\", { query })}</div>\n        )}\n        {windowed && <div aria-hidden style={{ height: offsets![first] }} data-slot=\"listbox-spacer\" />}\n        {options.slice(first, last).map((o, k) => {\n          const i = first + k;\n          const on = isSelected(o.value);\n          const prev = options[i - 1];\n          // headings only for the unsearched list, where they follow the caller's order; arrow keys never land on them\n          const groupStart = !query && (o.group ?? \"\") !== (prev?.group ?? \"\") && (i > 0 || !!o.group);\n          const detail = query && o.group ? [o.group, o.description].filter(Boolean).join(\" · \") : o.description;\n          const row = (\n            <>\n              {groupStart && i > 0 && (\n                <div role=\"presentation\" data-slot=\"listbox-separator\" className={overlay.separator} />\n              )}\n              {groupStart && o.group && (\n                <div role=\"presentation\" data-slot=\"listbox-group\" className={overlay.heading}>\n                  {o.group}\n                </div>\n              )}\n              <div\n                id={`${id}-${i}`}\n                role=\"option\"\n                aria-selected={on}\n                aria-disabled={o.disabled || undefined}\n                aria-description={!query && o.group ? o.group : undefined}\n                aria-setsize={windowed ? options.length : undefined}\n                aria-posinset={windowed ? i + 1 : undefined}\n                data-disabled={o.disabled ? \"\" : undefined}\n                data-active={i === active ? \"\" : undefined}\n                className={cn(overlay.item, \"cursor-pointer\", detail && \"py-1.5\")}\n                onPointerMove={() => {\n                  if (i !== active && !o.disabled) onActive(i);\n                }}\n                onMouseDown={(e) => e.preventDefault()}\n                onClick={() => {\n                  if (!o.disabled) onPick(o);\n                }}\n              >\n                {/* a checked row fills at once and its check scales in (a fade under reduced motion); unchecking\n                  lets the fill go over --pui-motion-out, like every hover */}\n                {multiple && (\n                  <span\n                    aria-hidden\n                    data-slot=\"listbox-check\"\n                    className={cn(\n                      \"grid size-4 shrink-0 place-items-center rounded-[calc(var(--pui-radius-chip)-2px)] shadow-[inset_0_0_0_1px_var(--pui-line-strong)] transition-[background-color,box-shadow] duration-[var(--pui-motion-out)]\",\n                      on && \"bg-accent text-[var(--pui-accent-foreground)] shadow-none duration-0\",\n                    )}\n                  >\n                    {on && <Check className=\"!size-3 animate-enter-scale !text-current\" strokeWidth={3} />}\n                  </span>\n                )}\n                {o.icon}\n                <span className=\"grid min-w-0 flex-1 gap-px leading-snug\">\n                  <span className=\"truncate\">\n                    <Highlight text={o.label} query={query} />\n                  </span>\n                  {detail && <span className=\"truncate text-xs text-ink-3\">{detail}</span>}\n                </span>\n                {!multiple && on && <Check data-slot=\"listbox-check\" className=\"animate-enter-scale !text-accent\" />}\n              </div>\n            </>\n          );\n          // a windowed row is measured with its heading, so the spacers stay true\n          return windowed ? (\n            <div key={o.value} role=\"presentation\" data-row={o.value}>\n              {row}\n            </div>\n          ) : (\n            <Fragment key={o.value}>{row}</Fragment>\n          );\n        })}\n        {windowed && (\n          <div aria-hidden style={{ height: offsets![options.length] - offsets![last] }} data-slot=\"listbox-spacer\" />\n        )}\n      </div>\n    </>\n  );\n}\n"},{"path":"beamline/components/_shared/match.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/match.tsx","content":"/** One way to rank and mark matches for every search-as-you-type list (command menu, combobox, multi-select). */\n\n/** How well `label` (and its keywords) answers `query`: label start 100, word start 80, anywhere 60, keyword 45,\n *  every word of a several-word query found across label, keywords and description 40 (50 when one starts the label:\n *  \"billing city\" finds Billing address › City), description 30, letters in order 15, no match 0. An empty query\n *  matches everything with 1. */\nexport function rank(query: string, label: string, keywords?: string[], description?: string): number {\n  const q = query.trim().toLowerCase();\n  if (!q) return 1;\n  const l = label.toLowerCase();\n  const at = l.indexOf(q);\n  if (at === 0) return 100 - l.length / 100;\n  if (at > 0 && /[\\s\\-/(_.]/.test(l[at - 1])) return 80;\n  if (at > 0) return 60;\n  if (keywords?.some((k) => k.toLowerCase().includes(q))) return 45;\n  const words = q.split(/\\s+/);\n  if (words.length > 1) {\n    const hay = [l, ...(keywords ?? []).map((k) => k.toLowerCase()), description?.toLowerCase() ?? \"\"];\n    if (words.every((w) => hay.some((h) => h.includes(w)))) return words.some((w) => l.startsWith(w)) ? 50 : 40;\n  }\n  if (description?.toLowerCase().includes(q)) return 30;\n  let i = 0;\n  for (const ch of l) if (ch === q[i]) i++;\n  return i === q.length ? 15 : 0;\n}\n\n/** Sorts by rank (best first, original order for ties) and drops non-matches. */\nexport function filterRanked<T>(\n  items: T[],\n  query: string,\n  get: (item: T) => { label: string; keywords?: string[]; description?: string },\n): T[] {\n  if (!query.trim()) return items;\n  return items\n    .map((item, order) => {\n      const g = get(item);\n      return { item, order, s: rank(query, g.label, g.keywords, g.description) };\n    })\n    .filter((r) => r.s > 0)\n    .sort((a, b) => b.s - a.s || a.order - b.order)\n    .map((r) => r.item);\n}\n\n/** The label with the matched run emphasised. */\nexport function Highlight({ text, query }: { text: string; query: string }) {\n  const q = query.trim().toLowerCase();\n  const at = q ? text.toLowerCase().indexOf(q) : -1;\n  if (at < 0) return <>{text}</>;\n  return (\n    <>\n      {text.slice(0, at)}\n      <mark className=\"bg-transparent font-medium text-ink\">{text.slice(at, at + q.length)}</mark>\n      {text.slice(at + q.length)}\n    </>\n  );\n}\n"},{"path":"beamline/components/_shared/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/record-list.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/record-list.ts","content":"\"use client\";\n\nimport { useMemo } from \"react\";\nimport type { ListFilters, ListProps, ListSort } from \"../../lib/list\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { useListWindow, useRetainedRows } from \"./list-state\";\n\nexport interface ListColumn<T> {\n  id: string;\n  value?: (row: T) => unknown;\n}\nconst NO_FILTERS: ListFilters = {};\nconst NO_SORT: ListSort = [];\nconst NO_SELECTION: string[] = [];\n\n/** One state owner for custom list views and the native Table. No network or persistence behavior. */\nexport function useListController(props: ListProps, aliases?: ListFilters) {\n  const paging = useListWindow(props);\n  const [query, changeQuery] = useControllable(props.query, props.defaultQuery ?? \"\", props.onQueryChange);\n  const [sort, changeSort] = useControllable(props.sort, props.defaultSort ?? NO_SORT, props.onSortChange);\n  const [storedFilters, changeFilters] = useControllable(\n    props.filters,\n    props.defaultFilters ?? NO_FILTERS,\n    props.onFiltersChange,\n  );\n  const filters = props.filters !== undefined || !aliases ? storedFilters : { ...storedFilters, ...aliases };\n  const [selected, setSelected] = useControllable(\n    props.selected,\n    props.defaultSelected ?? NO_SELECTION,\n    props.onSelectedChange,\n  );\n  const setQuery = (value: string) => {\n    changeQuery(value);\n    paging.resetPage();\n  };\n  const setSort = (value: ListSort) => {\n    changeSort(value);\n    paging.resetPage();\n  };\n  const setFilters = (value: ListFilters) => {\n    changeFilters(value);\n    paging.resetPage();\n  };\n  return {\n    ...paging,\n    query,\n    setQuery,\n    sort,\n    setSort,\n    filters,\n    setFilters,\n    selected,\n    setSelected,\n    error: props.error instanceof Error ? props.error.message : props.error,\n    props: {\n      ...props,\n      page: paging.page,\n      pageSize: paging.pageSize,\n      rowCount: paging.rowCount,\n      pagination: paging.pagination,\n      onPageChange: paging.setPage,\n      onPageSizeChange: paging.setPageSize,\n      query,\n      onQueryChange: changeQuery,\n      sort,\n      onSortChange: changeSort,\n      filters,\n      onFiltersChange: changeFilters,\n      selected,\n      onSelectedChange: setSelected,\n    },\n  };\n}\n\nexport function useRecordList<T>(props: ListProps, data: T[], columns?: ListColumn<T>[], aliases?: ListFilters) {\n  const state = useListController(props, aliases);\n  return useListRows(state, data, columns);\n}\n\n/** Apply the shared transforms after a custom screen has selected its local domain scope. */\nexport function useListRows<T>(\n  state: ReturnType<typeof useListController>,\n  data: T[],\n  columns?: ListColumn<T>[],\n  options?: { search?: boolean; sort?: boolean },\n) {\n  const props = state.props;\n  const { query, filters, sort } = state;\n  const retained = useRetainedRows(data, !!props.loading || !!props.loadingMore, props.error);\n  const matching = useMemo(() => {\n    if (props.manual) return retained;\n    const read = (row: T, id: string) => {\n      const column = columns?.find((c) => c.id === id);\n      return column?.value ? column.value(row) : (row as Record<string, unknown>)[id];\n    };\n    const q = options?.search === false ? \"\" : query.trim().toLowerCase();\n    const activeFilters = Object.entries(filters).filter(([, values]) => values?.length);\n    let result = retained;\n    if (q || activeFilters.length)\n      result = retained.filter((row) => {\n        const matchesQuery =\n          !q ||\n          (columns ? columns.map((column) => read(row, column.id)) : Object.values(row as object)).some((value) =>\n            String(value ?? \"\")\n              .toLowerCase()\n              .includes(q),\n          );\n        return (\n          matchesQuery &&\n          activeFilters.every(([id, values]) => {\n            if (columns && !columns.some((column) => column.id === id)) return true;\n            const value = read(row, id);\n            return Array.isArray(value)\n              ? value.some((item) => values.includes(String(item)))\n              : values.includes(String(value));\n          })\n        );\n      });\n    if (sort.length && options?.sort !== false)\n      result = [...result].sort((a, b) => {\n        for (const s of sort) {\n          if (columns && !columns.some((c) => c.id === s.id)) continue;\n          const av = read(a, s.id),\n            bv = read(b, s.id);\n          if (av == null && bv == null) continue;\n          if (av == null) return 1;\n          if (bv == null) return -1;\n          const cmp =\n            typeof av === \"number\" && typeof bv === \"number\"\n              ? av - bv\n              : String(av).localeCompare(String(bv), undefined, { numeric: true });\n          if (cmp) return s.desc ? -cmp : cmp;\n        }\n        return 0;\n      });\n    return result;\n  }, [retained, columns, query, sort, filters, props.manual, options?.search, options?.sort]);\n  const rows =\n    props.manual || !state.pagination\n      ? matching\n      : matching.slice((state.page - 1) * state.pageSize, state.page * state.pageSize);\n  const rowCount = props.manual ? state.rowCount : matching.length;\n  return { ...state, rows, rowCount };\n}\n"},{"path":"beamline/components/_shared/retained-rows.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/retained-rows.ts","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n/** Keep the last completed rows if a fetch library temporarily supplies an empty pending/error result. */\nexport function useRetainedRows<T>(data: T[], pending: boolean, error: unknown) {\n  // Retain committed responses without another render: callers may derive their rows inline.\n  const settled = useRef(data);\n  useEffect(() => {\n    if (!pending && !error) settled.current = data;\n  }, [data, pending, error]);\n  return (pending || error) && data.length === 0 ? settled.current : data;\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/use-open-from.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/use-open-from.ts","content":"\"use client\";\n\nimport { useCallback, type Ref } from \"react\";\n\n/**\n * A centred layer (dialog, alert dialog) that opens out of whatever opened it: as its content element attaches (each\n * time it opens, before the first frame and before focus moves in), the element that had focus (the button, row or\n * card that was used) is measured, and the layer starts as that box, offset back onto it, then grows to its own place\n * (overlay.module.css, `[data-open-from]`). The same values take it back there as it closes. Without a recent press\n * or Enter/Space (a shortcut, a controlled `open` from code) nothing is set and the ordinary entrance runs. Returns a ref\n * callback for the layer's content element, composed with the caller's `ref`.\n */\nexport function useOpenFrom<T extends HTMLElement>(external?: Ref<T>) {\n  watch();\n  return useCallback(\n    (el: T | null) => {\n      if (typeof external === \"function\") external(el);\n      else if (external) external.current = el;\n      if (el) measure(el);\n    },\n    [external],\n  );\n}\n\n/** What was last pressed or keyed, noted as it happened: a field inside the new layer may take focus (autoFocus)\n *  before the layer can ask what had it. */\nlet last: { el: Element; at: number } | null = null;\nlet watching = false;\nfunction watch() {\n  if (watching || typeof document === \"undefined\") return;\n  watching = true;\n  const note = (e: Event) => {\n    // a press, or Enter / Space on the focused control; a shortcut (⌘K) opens from nowhere in particular\n    if (e.type === \"keydown\" && ![\"Enter\", \" \"].includes((e as KeyboardEvent).key)) return;\n    // the control that was pressed, not the label or icon inside it\n    const target =\n      e.type === \"keydown\"\n        ? document.activeElement\n        : ((e.target as Element | null)?.closest(\n            \"button, a[href], [role=button], [role=menuitem], [role=option], [role=tab], [role=row], input, select, textarea, [tabindex]\",\n          ) ?? (e.target as Element | null));\n    if (target && target !== document.body) last = { el: target, at: performance.now() };\n  };\n  document.addEventListener(\"pointerdown\", note, true);\n  document.addEventListener(\"keydown\", note, true);\n}\n\nconst pending = new WeakSet<HTMLElement>();\nfunction measure(el: HTMLElement) {\n  // once per opening: a later call would measure the layer already moved onto its opener\n  if (el.dataset.openFrom !== undefined || pending.has(el)) return;\n  const doc = el.ownerDocument;\n  // only a recent press or activation counts as the opener; a layer opened by a shortcut or by code has none\n  const opener = (\n    last && performance.now() - last.at < 1500 && last.el.isConnected ? last.el : null\n  ) as HTMLElement | null;\n  if (!opener || opener === doc.body || el.contains(opener)) return;\n  // A modal's scroll lock lands in an effect after the ref. Measure on the next frame, after the lock and before\n  // paint. WebKit can reserve scrollbar space in the fixed containing block even when innerWidth === clientWidth\n  // (also on a touch viewport), so comparing those widths cannot tell us whether the centre will move.\n  const view = doc.defaultView;\n  if (view) {\n    pending.add(el);\n    view.requestAnimationFrame(() => {\n      pending.delete(el);\n      if (el.isConnected && el.dataset.openFrom === undefined && place(el, opener)) el.dataset.openFrom = \"\";\n    });\n  } else if (place(el, opener)) el.dataset.openFrom = \"\";\n}\n\n/** Sets the start box: the opener's box as offsets from the layer's resting box. False when either has no size. */\nfunction place(el: HTMLElement, opener: HTMLElement) {\n  const from = opener.getBoundingClientRect();\n  // the layer's own resting box: its centre as laid out (an entrance scale does not move a centre) and its size\n  // without transforms\n  const w = el.offsetWidth;\n  const h = el.offsetHeight;\n  if (!w || !h || !from.width || !from.height) return false;\n  const r = el.getBoundingClientRect();\n  const left = r.left + r.width / 2 - w / 2;\n  const top = r.top + r.height / 2 - h / 2;\n  el.style.setProperty(\"--from-x\", `${from.left - left}px`);\n  el.style.setProperty(\"--from-y\", `${from.top - top}px`);\n  el.style.setProperty(\"--from-t\", \"0px\");\n  el.style.setProperty(\"--from-l\", \"0px\");\n  el.style.setProperty(\"--from-r\", `${Math.max(0, w - from.width)}px`);\n  el.style.setProperty(\"--from-b\", `${Math.max(0, h - from.height)}px`);\n  return true;\n}\n"},{"path":"beamline/components/_shared/use-return-focus.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/use-return-focus.ts","content":"\"use client\";\n\nimport { useRef } from \"react\";\nimport { useAppearanceRoot } from \"../../runtime/appearance\";\n\n/**\n * Where focus goes when a modal layer closes. Radix returns it to its own Trigger; a layer opened another way (a\n * controlled `open`, a menu item, a shortcut) would drop focus on <body>. Spread the result on the Radix content: it\n * remembers what had focus as the layer opened and returns there, unless the caller's own handler prevents it.\n * If that opener is removed, return to its surviving owning region, or the managed scope's visual root.\n */\nexport function useReturnFocus({\n  onOpenAutoFocus,\n  onCloseAutoFocus,\n}: { onOpenAutoFocus?: (event: Event) => void; onCloseAutoFocus?: (event: Event) => void } = {}) {\n  track();\n  const before = useRef<HTMLElement | null>(null);\n  const regions = useRef<HTMLElement[]>([]);\n  const scope = useAppearanceRoot();\n  return {\n    onOpenAutoFocus: (event: Event) => {\n      before.current = outside(document.activeElement, event.currentTarget as Element | null);\n      const owners: HTMLElement[] = [];\n      let ancestor = before.current;\n      while (ancestor) {\n        if (ancestor !== document.body && ancestor.matches(\"[data-return-region]\")) owners.push(ancestor);\n        ancestor = ancestor.parentElement;\n      }\n      const root = scope ?? before.current?.closest<HTMLElement>(\".pui\");\n      if (root && root !== document.body && !owners.includes(root)) owners.push(root);\n      regions.current = owners;\n      onOpenAutoFocus?.(event);\n    },\n    /** `layer` is the layer's own element, for a caller that runs this itself instead of Radix. */\n    onCloseAutoFocus: (event: Event, layer: Element | null = event.currentTarget as Element | null) => {\n      onCloseAutoFocus?.(event);\n      if (event.defaultPrevented) return;\n      // a layer whose field took focus as it mounted (autoFocus) never hears that it opened (Radix skips the mount\n      // focus): the opener is then the last element focused outside it\n      const el =\n        before.current && before.current.isConnected && !layer?.contains?.(before.current)\n          ? before.current\n          : outside(null, layer);\n      if (el && el !== document.body && el.isConnected) {\n        event.preventDefault();\n        el.focus({ preventScroll: true });\n        return;\n      }\n      const region = regions.current.find((owner) => owner.isConnected);\n      if (region) {\n        event.preventDefault();\n        // Regions need not be tab stops, but they must receive programmatic return focus.\n        if (!region.hasAttribute(\"tabindex\")) region.tabIndex = -1;\n        region.focus({ preventScroll: true });\n      }\n    },\n  };\n}\n\n/** The last elements that took focus or were pressed, newest last, noted as it happened (capture phase, once per\n *  page). Presses count too: a click on a button does not focus it everywhere (Safari), and the control that was\n *  pressed is the opener a layer should give focus back to. */\nconst trail: Element[] = [];\nlet tracking = false;\nconst PRESSABLE =\n  \"button, a[href], [role=button], [role=menuitem], [role=option], [role=tab], [tabindex], input, select, textarea\";\nfunction note(el: Element | null) {\n  if (!el || el === document.body) return;\n  if (trail.at(-1) === el) return;\n  trail.push(el);\n  if (trail.length > 24) trail.shift();\n}\nfunction track() {\n  if (tracking || typeof document === \"undefined\") return;\n  tracking = true;\n  // A lazily mounted layer may be the first focus consumer on the page. Remember its opener before child autofocus.\n  note(document.activeElement);\n  document.addEventListener(\"focusin\", (event) => note(event.target as Element | null), true);\n  document.addEventListener(\n    \"pointerdown\",\n    (event) => note((event.target as Element | null)?.closest?.(PRESSABLE) ?? null),\n    true,\n  );\n}\n\n/** `active` unless it is inside `layer`; then the most recent element focused outside the layer that still exists. */\nfunction outside(active: Element | null, layer: Element | null): HTMLElement | null {\n  if (active && active !== document.body && !layer?.contains?.(active)) return active as HTMLElement;\n  for (let i = trail.length - 1; i >= 0; i--) {\n    const el = trail[i];\n    if (el.isConnected && el !== document.body && !layer?.contains?.(el)) return el as HTMLElement;\n  }\n  return null;\n}\n"},{"path":"beamline/components/alert-dialog/alert-dialog.tsx","type":"registry:file","target":"src/ui/beamline/components/alert-dialog/alert-dialog.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport * as RadixAlertDialog from \"@radix-ui/react-alert-dialog\";\nimport { Button, type ButtonVariant } from \"../button/button\";\nimport { useReturnFocus } from \"../_shared/use-return-focus\";\nimport { useOpenFrom } from \"../_shared/use-open-from\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\nimport { cn } from \"../../lib/utils\";\nimport overlay from \"../_shared/overlay.module.css\";\n\n/** A confirmation that must be answered. open / defaultOpen / onOpenChange. */\nexport const AlertDialog = RadixAlertDialog.Root;\nexport const AlertDialogTrigger = RadixAlertDialog.Trigger;\n\nexport interface AlertDialogClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the scrim part; no override by default. */\n  scrim?: string;\n  /** Classes for the header part; no override by default. */\n  header?: 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}\n\nexport interface AlertDialogContentProps extends Omit<ComponentProps<typeof RadixAlertDialog.Content>, \"title\"> {\n  /** The question: \"Revoke production-server?\" */\n  title: ReactNode;\n  /** What happens, and whether it can be undone. */\n  description: ReactNode;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: AlertDialogClassNames;\n}\n\n/**\n * Blocking: the scrim does not dismiss it (Escape and Cancel do), and focus starts on the safe choice, Cancel. Put an\n * AlertDialogFooter with AlertDialogCancel then AlertDialogAction inside.\n */\nexport function AlertDialogContent({\n  title,\n  description,\n  className,\n  classNames,\n  children,\n  onCloseAutoFocus,\n  ref,\n  ...props\n}: AlertDialogContentProps) {\n  const openFrom = useOpenFrom(ref);\n  const container = usePortalContainer();\n  // focus starts on the safe choice, Cancel\n  const focus = useReturnFocus({\n    onOpenAutoFocus: (e) => {\n      e.preventDefault();\n      (e.currentTarget as HTMLElement).querySelector<HTMLElement>(\"[data-slot=alert-dialog-cancel]\")?.focus();\n    },\n    onCloseAutoFocus,\n  });\n  if (container === null) return null;\n  return (\n    <RadixAlertDialog.Portal container={container}>\n      <RadixAlertDialog.Overlay data-slot=\"alert-dialog-scrim\" className={cn(overlay.scrim, classNames?.scrim)} />\n      <RadixAlertDialog.Content\n        ref={openFrom}\n        data-slot=\"alert-dialog\"\n        className={cn(\n          overlay.dialog,\n          \"grid gap-4 p-[var(--pui-pad-panel)] [--pui-dialog-width:400px]\",\n          classNames?.root,\n          className,\n        )}\n        {...focus}\n        {...props}\n      >\n        <div data-slot=\"alert-dialog-header\" className={cn(\"grid gap-1.5\", classNames?.header)}>\n          <RadixAlertDialog.Title\n            data-slot=\"alert-dialog-title\"\n            className={cn(\"text-subheading leading-snug text-ink\", classNames?.title)}\n          >\n            {title}\n          </RadixAlertDialog.Title>\n          <RadixAlertDialog.Description\n            data-slot=\"alert-dialog-description\"\n            className={cn(\"text-sm leading-relaxed text-ink-2\", classNames?.description)}\n          >\n            {description}\n          </RadixAlertDialog.Description>\n        </div>\n        {children}\n      </RadixAlertDialog.Content>\n    </RadixAlertDialog.Portal>\n  );\n}\n\n/** Cancel, then the consequential action, at the end; in a narrow dialog (a phone) they share the row in equal parts. */\nexport function AlertDialogFooter({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-dialog-footer\"\n      className={cn(\"mt-1 flex flex-wrap justify-end gap-2 @max-xs:*:flex-1\", className)}\n      {...props}\n    />\n  );\n}\n\n/** The safe choice; it has focus when the dialog opens. */\nexport function AlertDialogCancel({\n  children: childrenProp,\n  ...props\n}: ComponentProps<typeof RadixAlertDialog.Cancel>) {\n  const i18n = useMessages(\"alert-dialog\", englishMessages);\n  const children = childrenProp === undefined ? i18n(\"cancel\") : childrenProp;\n\n  return (\n    <RadixAlertDialog.Cancel asChild {...props}>\n      <Button data-slot=\"alert-dialog-cancel\" variant=\"secondary\">\n        {children}\n      </Button>\n    </RadixAlertDialog.Cancel>\n  );\n}\n\n/** The consequential choice: danger for destructive actions, primary otherwise. Your onClick runs the operation; for\n *  async work call event.preventDefault() to keep the dialog open, show loading, and close it on success. */\nexport function AlertDialogAction({\n  variant = \"danger\",\n  loading,\n  children,\n  ...props\n}: ComponentProps<typeof RadixAlertDialog.Action> & {\n  /** Visual weight of the control. Defaults to \"danger\". */\n  variant?: ButtonVariant;\n  /** Shows the loading state while data or an operation is pending. Defaults to false. */\n  loading?: boolean;\n}) {\n  return (\n    <RadixAlertDialog.Action asChild {...props}>\n      <Button data-slot=\"alert-dialog-action\" variant={variant} loading={loading}>\n        {children}\n      </Button>\n    </RadixAlertDialog.Action>\n  );\n}\n\nexport default AlertDialog;\n"},{"path":"beamline/components/alert-dialog/messages.ts","type":"registry:file","target":"src/ui/beamline/components/alert-dialog/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  cancel: \"Cancel\",\n} satisfies MessageDefinitions;\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/avatar/avatar.module.css","type":"registry:file","target":"src/ui/beamline/components/avatar/avatar.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Avatar: a face (initials on the name's hue, or a photo) and an optional presence mark. The mark is cut out of the\n     face with a mask, so the gap around it is truly empty and reads on any ground: a card, a row hover, glass, a photo. */\n  .root {\n    --_s: 36px;\n    --_d: 10px;\n    --_g: 2px;\n    position: relative;\n    display: inline-grid;\n    flex: none;\n    width: var(--_s);\n    height: var(--_s);\n    place-items: center;\n    /* initials scale with the face (geometry), never below 80% of the smallest type step */\n    font-size: max(calc(var(--pui-text-2xs) * 0.8), calc(var(--_s) * 0.36));\n    font-weight: var(--pui-weight-medium);\n    line-height: 1;\n    user-select: none;\n  }\n  .xs {\n    --_s: 20px;\n    --_d: 6px;\n    --_g: 1.5px;\n  }\n  .sm {\n    --_s: 28px;\n    --_d: 8px;\n  }\n  .lg {\n    --_s: 44px;\n    --_d: 12px;\n  }\n  .xl {\n    --_s: 56px;\n    --_d: 14px;\n    --_g: 2.5px;\n  }\n  .face {\n    position: absolute;\n    inset: 0;\n    display: grid;\n    place-items: center;\n    overflow: hidden;\n    border-radius: 50%;\n    background: color-mix(in oklab, var(--pui-avatar-hue) 24%, var(--pui-surface-2));\n    color: color-mix(in oklab, var(--pui-avatar-hue) 55%, var(--pui-ink));\n  }\n  /* the initials keep their tint but mix it into black or white, whichever the face's fill reads better against (its\n     luminance at WCAG's 0.179 crossover): mixed into the scope's ink they fell to 3.5–4.5 on mid fills, a brand-toned\n     sidebar's above all; this keeps every face at 5.5:1 or more in every look */\n  @supports (color: color(from red xyz-d65 x y z)) {\n    .face {\n      --_ink: color(\n        from color-mix(in oklab, var(--pui-avatar-hue) 24%, var(--pui-surface-2)) xyz-d65\n          calc(0.9505 * round(0.679 - y)) round(0.679 - y) calc(1.089 * round(0.679 - y))\n      );\n      color: color-mix(in oklab, var(--pui-avatar-hue) 55%, var(--_ink));\n    }\n  }\n  /* a company tile takes the look's card corner, never more than a third of the face, so it stays a square at xs */\n  .root[data-shape=\"square\"] .face {\n    /* nesting input (O1 §14.1, as Button): a parent that holds this near its rounded corner sets --pui-nest-radius and\n       --pui-nest-inset; the corner becomes radius − inset, clamped between a floor (chip) and the part's own role radius (or --pui-nest-ceiling: a pill container sets it to the pill radius). Unset, the declaration is invalid and the own corner\n       applies. --pui-avatar-radius wins over both. */\n    --_avatar-nest: clamp(\n      var(--pui-radius-chip),\n      calc(var(--pui-nest-radius) - var(--pui-nest-inset, 0px)),\n      var(--pui-nest-ceiling, min(var(--pui-radius-card), calc(var(--_s) / 3)))\n    );\n    --_r: var(--pui-avatar-radius, var(--_avatar-nest, min(var(--pui-radius-card), calc(var(--_s) / 3))));\n    border-radius: var(--_r);\n  }\n  .photo {\n    position: absolute;\n    inset: 0;\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    transition: opacity var(--pui-fade-in) var(--pui-ease-default);\n  }\n  /* still on its way: the initials hold the place, and the photo fades in over them when it lands */\n  .photo[data-pending] {\n    opacity: 0;\n  }\n  /* The mark sits on the bottom inline-end corner; the face loses a disc one gap wider than the mark under it. */\n  .root[data-status] .face {\n    --_at: calc(var(--_d) / 2 - 1px);\n    mask: radial-gradient(\n      circle at calc(100% - var(--_at)) calc(100% - var(--_at)),\n      transparent calc(var(--_d) / 2 + var(--_g)),\n      black calc(var(--_d) / 2 + var(--_g) + 0.5px)\n    );\n  }\n  .root[data-status]:dir(rtl) .face {\n    mask: radial-gradient(\n      circle at var(--_at) calc(100% - var(--_at)),\n      transparent calc(var(--_d) / 2 + var(--_g)),\n      black calc(var(--_d) / 2 + var(--_g) + 0.5px)\n    );\n  }\n  .mark {\n    position: absolute;\n    inset-inline-end: -1px;\n    bottom: -1px;\n  }\n  /* a presence that changed settles into its new mark: the colour and shape arrive at once, the mark grows from a\n     little smaller (reduced motion keeps only the fade) */\n  .mark[data-changed] {\n    animation: presence-settle var(--pui-duration-feedback) var(--pui-ease-standard) both;\n  }\n\n  /* Presence: colour and shape together, so it reads in greyscale (DESIGN-LANGUAGE: colour is never the only signal).\n     online a full disc · away a crescent · busy a disc with a bar through it · offline a hollow ring. */\n  .presence {\n    --_d: 10px;\n    display: inline-block;\n    flex: none;\n    width: var(--_d);\n    height: var(--_d);\n    border-radius: 50%;\n    background: var(--_c);\n  }\n  .presence[data-size=\"xs\"] {\n    --_d: 6px;\n  }\n  .presence[data-size=\"sm\"] {\n    --_d: 8px;\n  }\n  .presence[data-size=\"lg\"] {\n    --_d: 12px;\n  }\n  .presence[data-size=\"xl\"] {\n    --_d: 14px;\n  }\n  .presence[data-status=\"online\"] {\n    --_c: var(--pui-green);\n  }\n  .presence[data-status=\"away\"] {\n    --_c: var(--pui-orange);\n    mask: radial-gradient(circle at 18% 18%, transparent calc(var(--_d) * 0.36), black calc(var(--_d) * 0.36 + 0.5px));\n  }\n  .presence[data-status=\"away\"]:dir(rtl) {\n    mask: radial-gradient(circle at 82% 18%, transparent calc(var(--_d) * 0.36), black calc(var(--_d) * 0.36 + 0.5px));\n  }\n  .presence[data-status=\"busy\"] {\n    --_c: var(--pui-red);\n    mask:\n      linear-gradient(black 0 0) center / 56% max(1.5px, 22%) no-repeat,\n      linear-gradient(black 0 0);\n    mask-composite: exclude;\n  }\n  .presence[data-status=\"offline\"] {\n    --_c: var(--pui-ink-3);\n    mask: radial-gradient(circle, transparent calc(var(--_d) * 0.24), black calc(var(--_d) * 0.24 + 0.5px));\n  }\n}\n@keyframes presence-settle {\n  from {\n    opacity: 0.4;\n    scale: calc(1 - 0.45 * var(--pui-motion-travel, 1));\n  }\n}\n"},{"path":"beamline/components/avatar/avatar.tsx","type":"registry:file","target":"src/ui/beamline/components/avatar/avatar.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { AvatarDefaults } from \"./defaults\";\nexport type { AvatarDefaults } from \"./defaults\";\n\nimport { useCallback, useState, type CSSProperties, type HTMLAttributes, type Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./avatar.module.css\";\n\nexport type AvatarSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\nexport type PresenceStatus = \"online\" | \"away\" | \"busy\" | \"offline\";\n\nconst SIZE: Record<AvatarSize, string | undefined> = {\n  xs: styles.xs,\n  sm: styles.sm,\n  md: undefined,\n  lg: styles.lg,\n  xl: styles.xl,\n};\n\n/** A person's (or account's) series hue, the same everywhere their name appears. */\nexport function hueOf(name: string) {\n  let h = 0;\n  for (const ch of name) h = (h * 31 + ch.charCodeAt(0)) | 0;\n  return `var(--pui-series-${(Math.abs(h) % 8) + 1})`;\n}\nexport const initialsOf = (name: string) =>\n  name\n    .trim()\n    .split(/\\s+/)\n    .filter((w) => /\\p{L}|\\d/u.test(w[0] ?? \"\"))\n    .slice(0, 2)\n    .map((w) => w[0].toUpperCase())\n    .join(\"\") || \"?\";\n\n/** A presence mark for avatars and lists. Colour and shape together: online a disc, away a crescent, busy a disc with\n *  a bar, offline a hollow ring, so the status survives greyscale and colour-blindness. */\nexport function PresenceDot({\n  status,\n  size = \"md\",\n  className,\n  style,\n  ref,\n  ...props\n}: {\n  status: PresenceStatus;\n  size?: AvatarSize;\n  className?: string;\n  style?: CSSProperties;\n  ref?: Ref<HTMLSpanElement>;\n} & Omit<HTMLAttributes<HTMLSpanElement>, \"children\">) {\n  const i18n = useMessages(\"avatar\", englishMessages);\n  return (\n    <span\n      {...props}\n      style={style}\n      ref={ref}\n      role=\"img\"\n      aria-label={props[\"aria-label\"] ?? i18n(status)}\n      data-slot=\"presence-dot\"\n      data-status={status}\n      data-size={size}\n      className={cn(styles.presence, className)}\n    />\n  );\n}\n\nexport interface AvatarClassNames {\n  root?: string;\n  face?: string;\n  photo?: string;\n  presence?: string;\n}\n\nexport interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {\n  name: string;\n  /** A photo; initials show while it loads and if it fails. */\n  src?: string;\n  size?: AvatarSize;\n  /** circle for people, square for companies and workspaces. */\n  shape?: \"circle\" | \"square\";\n  status?: PresenceStatus;\n  ref?: Ref<HTMLSpanElement>;\n  classNames?: AvatarClassNames;\n}\n\n/** A person or account in a small space: a photo, or initials on the colour their name always gets. A photo still on\n *  its way fades in over the initials instead of popping; a presence that changes settles into its new mark. */\nexport function Avatar({\n  name,\n  src,\n  size: sizeProp,\n  shape = \"circle\",\n  status,\n  className,\n  classNames,\n  style,\n  ref,\n  ...props\n}: AvatarProps) {\n  const i18n = useMessages(\"avatar\", englishMessages);\n  const scopeDefaults = useComponentDefaults<AvatarDefaults>(\"avatar\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const [failed, setFailed] = useState<string | null>(null);\n  const photo = src && failed !== src;\n  // a photo that is not decoded yet when it mounts waits unseen and fades in over the initials when it lands; one that\n  // is already there (cached, or loaded before the page hydrated) shows at once, so a server render never hides it\n  const [pending, setPending] = useState<string | null>(null);\n  const watch = useCallback((img: HTMLImageElement | null) => {\n    if (!img) return;\n    if (!img.complete) setPending(img.getAttribute(\"src\"));\n    // A cached/decode failure can finish before React attaches onError during hydration.\n    else if (img.naturalWidth === 0) setFailed(img.getAttribute(\"src\"));\n  }, []);\n  // a presence that changes (online → away) settles into its new mark; the first render does not play it\n  const [seen, setSeen] = useState(status);\n  const [changes, setChanges] = useState(0);\n  if (seen !== status) {\n    setSeen(status);\n    setChanges((n) => n + 1);\n  }\n  return (\n    <span\n      ref={ref}\n      data-slot=\"avatar\"\n      data-shape={shape}\n      data-status={status}\n      role=\"img\"\n      aria-label={status ? i18n(\"presence\", { name, status: i18n(status) }) : name}\n      {...props}\n      className={cn(styles.root, SIZE[size], classNames?.root, className)}\n      style={{ \"--pui-avatar-hue\": hueOf(name), ...style } as CSSProperties}\n    >\n      <span aria-hidden data-slot=\"avatar-face\" className={cn(styles.face, classNames?.face)}>\n        {initialsOf(name)}\n        {photo && (\n          <img\n            key={src}\n            ref={watch}\n            data-slot=\"avatar-photo\"\n            data-pending={pending === src || undefined}\n            src={src}\n            alt=\"\"\n            draggable={false}\n            onLoad={() => setPending(null)}\n            onError={() => setFailed(src)}\n            className={cn(styles.photo, classNames?.photo)}\n          />\n        )}\n      </span>\n      {status && (\n        <PresenceDot\n          key={changes}\n          status={status}\n          size={size}\n          data-changed={changes > 0 || undefined}\n          className={cn(styles.mark, classNames?.presence)}\n        />\n      )}\n    </span>\n  );\n}\n\nexport default Avatar;\n"},{"path":"beamline/components/avatar/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/avatar/defaults.ts","content":"/** Pure allowed presentation defaults for Avatar; no component or runtime imports. */\nexport interface AvatarDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"xs\" | \"lg\" | \"xl\";\n}\n"},{"path":"beamline/components/avatar/messages.ts","type":"registry:file","target":"src/ui/beamline/components/avatar/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  online: \"Online\",\n  away: \"Away\",\n  busy: \"Do not disturb\",\n  offline: \"Offline\",\n  presence: \"{name}, {status}\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/badge/badge.module.css","type":"registry:file","target":"src/ui/beamline/components/badge/badge.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Badge: a tinted label. The tone sets three local colours; the tag role lets a look outline it and ink its text\n     (pop turns badges into stickers), every other look leaves it a quiet tinted pill for dense tables. */\n  .root {\n    --pui-badge-bg: var(--pui-hover);\n    --pui-badge-fg: var(--pui-ink-2);\n    --pui-badge-dot: var(--pui-ink-3);\n    display: inline-flex;\n    flex-shrink: 0;\n    max-width: 100%;\n    align-items: center;\n    gap: var(--pui-space-1);\n    height: 22px;\n    padding-inline: var(--pui-space-2);\n    /* nesting input (O1 §14.1, as Button): a parent that holds this near its rounded corner sets --pui-nest-radius and\n       --pui-nest-inset; the corner becomes radius − inset, clamped between a floor (chip) and the part's own role radius (or --pui-nest-ceiling: a pill container sets it to the pill radius). Unset, the declaration is invalid and the own corner\n       applies. --pui-badge-radius wins over both. */\n    --_badge-nest: clamp(\n      var(--pui-radius-chip),\n      calc(var(--pui-nest-radius) - var(--pui-nest-inset, 0px)),\n      var(--pui-nest-ceiling, var(--pui-radius-pill))\n    );\n    border-radius: var(--pui-badge-radius, var(--_badge-nest, var(--pui-radius-pill)));\n    background: var(--pui-badge-bg);\n    color: var(--m-tag-ink, var(--pui-badge-fg));\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-medium);\n    font-variant-numeric: tabular-nums;\n    line-height: 1;\n    white-space: nowrap;\n    /* a tone that changes (Pending → Paid) eases its fill and ink in place; the word itself changes at once */\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      var(--m-transition);\n  }\n  /* in a cell too narrow for it the label ends in \"…\" (the whole label shows on hover); icon and dot stay */\n  .label {\n    min-width: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    /* the clip keeps descenders: the label is one line tall at the badge's line height */\n    padding-block: 0.1em;\n  }\n  .root :where(svg) {\n    width: 14px;\n    height: 14px;\n    flex-shrink: 0;\n  }\n  .sm {\n    height: 20px;\n    padding-inline: var(--pui-space-1-5);\n    font-size: var(--pui-text-2xs);\n  }\n  .sm :where(svg) {\n    width: 12px;\n    height: 12px;\n  }\n  .accent {\n    --pui-badge-bg: var(--pui-accent-tint);\n    --pui-badge-fg: var(--pui-accent-ink);\n    --pui-badge-dot: var(--pui-accent);\n  }\n  .success {\n    --pui-badge-bg: var(--pui-green-tint);\n    --pui-badge-fg: var(--pui-green);\n    --pui-badge-dot: var(--pui-green);\n  }\n  .warning {\n    --pui-badge-bg: var(--pui-orange-tint);\n    --pui-badge-fg: var(--pui-orange);\n    --pui-badge-dot: var(--pui-orange);\n  }\n  .danger {\n    --pui-badge-bg: var(--pui-red-tint);\n    --pui-badge-fg: var(--pui-red);\n    --pui-badge-dot: var(--pui-red);\n  }\n  .info {\n    --pui-badge-bg: var(--pui-blue-tint);\n    --pui-badge-fg: var(--pui-blue);\n    --pui-badge-dot: var(--pui-blue);\n  }\n  /* a category's own colour (identity): its dot, larger than a status dot so the colour reads, ringed in the badge's\n     own ink so a pale or a dark stop shows on any fill; an icon takes the colour instead; with tint the pill takes a light\n     wash of it and the word stays in ink */\n  .root[data-color] {\n    --pui-badge-dot: var(--_label-color);\n  }\n  .root[data-color] .dot {\n    width: 8px;\n    height: 8px;\n    box-shadow: 0 0 0 1px color-mix(in oklab, currentColor 30%, transparent);\n  }\n  .root[data-color] > :where(svg) {\n    color: var(--_label-color);\n  }\n  .root[data-color=\"tint\"] {\n    --pui-badge-bg: color-mix(in oklab, var(--_label-color) 18%, var(--pui-surface));\n    --pui-badge-fg: var(--pui-ink);\n  }\n  .dot {\n    width: 6px;\n    height: 6px;\n    flex-shrink: 0;\n    border-radius: var(--pui-radius-pill);\n    background: var(--pui-badge-dot);\n    transition: background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.root {\n  composes: m-tag from global;\n}\n"},{"path":"beamline/components/badge/badge.tsx","type":"registry:file","target":"src/ui/beamline/components/badge/badge.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { BadgeDefaults } from \"./defaults\";\nexport type { BadgeDefaults } from \"./defaults\";\n\nimport type { CSSProperties, HTMLAttributes, PointerEvent, ReactNode, Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./badge.module.css\";\n\nexport type BadgeTone = \"neutral\" | \"accent\" | \"success\" | \"warning\" | \"danger\" | \"info\";\nexport type BadgeSize = \"sm\" | \"md\";\n\nexport interface BadgeClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the dot part; no override by default. */\n  dot?: string;\n  /** Classes for the label part; no override by default. */\n  label?: string;\n}\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n  /** Semantic colour; pair it with a word or icon (colour is never the only signal). */\n  tone?: BadgeTone;\n  /** Height 20 / 22 px. */\n  size?: BadgeSize;\n  /** A leading icon element. */\n  icon?: ReactNode;\n  /** A leading status dot in the tone colour, instead of an icon. */\n  dot?: boolean;\n  /** A category's own colour, for a label the user matches across places (a card's \"Billing\" and the filter that\n   *  shows it): a dot in it before the word, ringed in the badge's ink so a pale or dark colour reads on any surface.\n   *  Take it from `identity(i, n)` in `@/lib/tone`, never a status tone. Omitted by default: no dot. */\n  color?: string;\n  /** With `color`, the pill takes a light tint of it as well (the word stays in ink). Default false: a neutral pill. */\n  tint?: boolean;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLSpanElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: BadgeClassNames;\n}\n\n/** A short label for status, category or count; a category the user matches across places takes `color` (a dot,\n *  optionally a tint, from the identity ramp). A tone that changes (Pending → Paid) eases its colours in place;\n *  nothing else moves, so it costs nothing in a dense table. In a cell too narrow for it, the label ends in \"…\" and the\n *  whole label shows on hover. Reads scoped presentation defaults. A look may outline it (pop makes it a sticker). */\nexport function Badge({\n  tone: toneProp,\n  size: sizeProp,\n  icon,\n  dot = false,\n  color,\n  tint = false,\n  className,\n  classNames,\n  children,\n  ref,\n  onPointerEnter,\n  ...props\n}: BadgeProps) {\n  const scopeDefaults = useComponentDefaults<BadgeDefaults>(\"badge\", [\"tone\", \"size\"]);\n  const tone = toneProp ?? scopeDefaults.tone ?? \"neutral\";\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  // a label cut by a narrow cell names itself on hover; nothing is measured until someone points at it\n  const nameIfCut = (e: PointerEvent<HTMLSpanElement>) => {\n    onPointerEnter?.(e);\n    const label = e.currentTarget.querySelector<HTMLElement>(\"[data-slot=badge-label]\");\n    if (!label || props.title !== undefined) return;\n    e.currentTarget.title = label.scrollWidth > label.clientWidth + 1 ? (label.textContent ?? \"\") : \"\";\n  };\n  return (\n    <span\n      ref={ref}\n      data-slot=\"badge\"\n      data-tone={tone}\n      data-color={color ? (tint ? \"tint\" : \"dot\") : undefined}\n      onPointerEnter={nameIfCut}\n      {...props}\n      style={color ? ({ \"--_label-color\": color, ...props.style } as CSSProperties) : props.style}\n      className={cn(\n        styles.root,\n        size === \"sm\" && styles.sm,\n        tone !== \"neutral\" && styles[tone],\n        classNames?.root,\n        className,\n      )}\n    >\n      {(dot || (color && !icon)) && (\n        <span data-slot=\"badge-dot\" aria-hidden className={cn(styles.dot, classNames?.dot)} />\n      )}\n      {icon}\n      {children != null && children !== false && (\n        <span data-slot=\"badge-label\" className={cn(styles.label, classNames?.label)}>\n          {children}\n        </span>\n      )}\n    </span>\n  );\n}\n\nexport default Badge;\n"},{"path":"beamline/components/badge/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/badge/defaults.ts","content":"/** Pure allowed presentation defaults for Badge; no component or runtime imports. */\nexport interface BadgeDefaults {\n  /** Shared tone setting; omit to inherit the surrounding scope or the component default. */\n  tone?: \"danger\" | \"accent\" | \"success\" | \"warning\" | \"info\" | \"neutral\";\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\n"},{"path":"beamline/components/bottom-sheet/bottom-sheet.tsx","type":"registry:file","target":"src/ui/beamline/components/bottom-sheet/bottom-sheet.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type Ref, type ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { animate, motion, useMotionValue, useTransform } from \"motion/react\";\nimport * as RadixDialog from \"@radix-ui/react-dialog\";\nimport { Button } from \"../button/button\";\nimport { useReturnFocus } from \"../_shared/use-return-focus\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useDrag } from \"../../hooks/use-drag\";\nimport { useResolvedAppearance } from \"../../hooks/use-resolved-appearance\";\nimport { motionEase } from \"../../lib/motion-tokens\";\nimport { semanticMotion } from \"../../lib/motion-source\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport overlay from \"../_shared/overlay.module.css\";\n\nexport interface BottomSheetProps {\n  /** An element that opens it (uncontrolled use). */\n  trigger?: ReactNode;\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  /** Heading shown above the content. Required. */\n  title: string;\n  /** Supporting text beneath the label or heading. Defaults to no supporting text. */\n  description?: string;\n  /** Resting heights as fractions of the screen, lowest first. Default [0.45, 0.92]. */\n  detents?: number[];\n  /** Which resting height it opens at. Default 0. */\n  initialDetent?: number;\n  /** Reports sheet resting height after a change; no callback by default. */\n  onDetentChange?: (index: number) => void;\n  /** Accessible text for the close control. Defaults to \"Close\". */\n  closeLabel?: string;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: Partial<\n    Record<\"root\" | \"backdrop\" | \"header\" | \"handle\" | \"title\" | \"description\" | \"close\" | \"body\", string>\n  >;\n  /** Content rendered inside the component. Defaults to no children. */\n  children?: ReactNode;\n}\n\n/**\n * A phone panel that rests at a peek and follows the finger to full height. Drag the handle (or the header) between\n * resting heights; a flick down from the lowest closes it. The handle is a button: Enter cycles heights, ↑ ↓ move.\n * Escape, the scrim and Close also close it: focus goes back at once and the sheet slides away after.\n */\nexport function BottomSheet({\n  trigger,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  title,\n  description,\n  detents = [0.45, 0.92],\n  initialDetent = 0,\n  onDetentChange,\n  closeLabel: closeLabelProp,\n  className,\n  classNames,\n  style,\n  ref,\n  children,\n}: BottomSheetProps) {\n  const i18n = useMessages(\"bottom-sheet\", englishMessages);\n  const closeLabel = closeLabelProp === undefined ? i18n(\"close\") : closeLabelProp;\n\n  const [open, setOpen] = useControllable(openProp, defaultOpen, onOpenChange);\n  const container = usePortalContainer();\n  // the panel outlives `open`: focus returns and the trap lets go as `open` turns false, the panel leaves after;\n  // opening again starts a fresh panel\n  const [shown, setShown] = useState(open);\n  const [round, setRound] = useState(0);\n  const [wasOpen, setWasOpen] = useState(open);\n  if (open !== wasOpen) {\n    setWasOpen(open);\n    if (open) {\n      setShown(true);\n      setRound((r) => r + 1);\n    }\n  }\n  return (\n    <RadixDialog.Root open={open} onOpenChange={setOpen}>\n      {trigger && <RadixDialog.Trigger asChild>{trigger}</RadixDialog.Trigger>}\n      {container !== null && shown && (\n        <RadixDialog.Portal container={container} forceMount>\n          <Panel\n            key={round}\n            open={open}\n            title={title}\n            description={description}\n            detents={detents}\n            initialDetent={initialDetent}\n            onDetentChange={onDetentChange}\n            closeLabel={closeLabel}\n            className={className}\n            classNames={classNames}\n            style={style}\n            ref={ref}\n            onClose={() => setOpen(false)}\n            onGone={() => setShown(false)}\n          >\n            {children}\n          </Panel>\n        </RadixDialog.Portal>\n      )}\n    </RadixDialog.Root>\n  );\n}\n\nfunction Panel({\n  open,\n  title,\n  description,\n  detents,\n  initialDetent,\n  onDetentChange,\n  closeLabel,\n  className,\n  classNames,\n  style,\n  ref,\n  onClose,\n  onGone,\n  children,\n}: Required<Pick<BottomSheetProps, \"title\" | \"detents\" | \"initialDetent\" | \"closeLabel\">> &\n  Pick<\n    BottomSheetProps,\n    \"description\" | \"onDetentChange\" | \"className\" | \"classNames\" | \"style\" | \"ref\" | \"children\"\n  > & { open: boolean; onClose: () => void; onGone: () => void }) {\n  const i18n = useMessages(\"bottom-sheet\", englishMessages);\n\n  const host = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(host, ref);\n  const appearance = useResolvedAppearance(host, { times: [\"duration-considered\"], easings: [\"ease-drawer\"] });\n  const entered = useRef(false);\n  const [vh, setVh] = useState(() => (typeof window === \"undefined\" ? 800 : window.innerHeight));\n  const maxH = Math.round(vh * detents[detents.length - 1]);\n  const yOf = (i: number) => maxH - Math.round(vh * detents[i]);\n  const [detent, setDetent] = useState(Math.min(initialDetent, detents.length - 1));\n  const y = useMotionValue(maxH);\n  const scrim = useTransform(y, [yOf(0), maxH], [1, 0]);\n  const drag = useRef<{ from: number } | null>(null);\n  // held: the handle answers the finger at once (wider and brighter) for as long as the sheet is held\n  const [held, setHeld] = useState(false);\n  const header = useRef<HTMLDivElement>(null);\n  // focus goes back as the sheet closes (below), so the unmount after it leaves must not move focus again\n  const released = useRef(false);\n  const focus = useReturnFocus({\n    onCloseAutoFocus: (event) => {\n      if (released.current) event.preventDefault();\n    },\n  });\n  // closed from inside (Escape, Close, the scrim, a flick): it slides away; closed by its owner: it goes at once\n  const [dismissed, setDismissed] = useState(false);\n  const away = !open && !dismissed;\n  const [headerH, setHeaderH] = useState(76);\n  useLayoutEffect(() => {\n    const element = header.current;\n    if (!element) return;\n    const measure = () => setHeaderH(element.offsetHeight);\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, []);\n\n  useEffect(() => {\n    const on = () => setVh(window.innerHeight);\n    window.addEventListener(\"resize\", on);\n    return () => window.removeEventListener(\"resize\", on);\n  }, []);\n  // the glide in flight, so a hand that grabs the sheet mid-glide takes it where it is\n  const gliding = useRef<ReturnType<typeof animate> | null>(null);\n  const go = (target: number, after?: () => void) => {\n    gliding.current?.stop();\n    const run = animate(y, target, {\n      duration: appearance.reducedMotion\n        ? 0\n        : (appearance.times[\"duration-considered\"] ?? semanticMotion.duration.considered),\n      // the drawer curve the scope resolves; before it has, the system's entrance curve\n      ease: motionEase(appearance.easings[\"ease-drawer\"], semanticMotion.ease.enter),\n    });\n    gliding.current = run;\n    if (after) run.then(after);\n  };\n  const settle = (i: number) => {\n    setDetent(i);\n    onDetentChange?.(i);\n    go(yOf(i));\n  };\n  const close = () => {\n    setDismissed(true);\n    onClose();\n  };\n  useLayoutEffect(() => {\n    if (!appearance.ready || entered.current) return;\n    entered.current = true;\n    y.set(maxH);\n    go(yOf(detent));\n  }, [appearance.ready]); // eslint-disable-line react-hooks/exhaustive-deps -- slide in once\n  useEffect(() => {\n    if (!drag.current && open) y.set(yOf(detent));\n  }, [vh]); // eslint-disable-line react-hooks/exhaustive-deps -- keep the resting height on rotation\n  // `open` turned false and the trap has let go: focus returns now (not after the slide, never through <body>), then\n  // the panel leaves; an effect, so the trap's own cleanup has already run\n  useEffect(() => {\n    if (open) return;\n    const active = document.activeElement;\n    if (!active || active === document.body || host.current?.contains(active))\n      focus.onCloseAutoFocus(new Event(\"close\", { cancelable: true }), host.current);\n    released.current = true;\n    // a leaving panel takes no focus back: not by Tab, not by a child's autofocus that lands a frame late\n    if (host.current) host.current.inert = true;\n    if (!dismissed) {\n      onGone();\n      return;\n    }\n    entered.current = true;\n    let live = true;\n    go(maxH, () => live && onGone());\n    return () => {\n      live = false;\n    };\n  }, [open]); // eslint-disable-line react-hooks/exhaustive-deps -- on close only\n\n  // the header drags the sheet through the shared drag (BUILDING §7): it follows the hand, resisting past the top; a\n  // release projects 180 ms of the throw and closes past half the way down (or on a fast flick from the lowest detent),\n  // else settles on the nearest detent; Escape mid-drag puts the sheet back on its detent and keeps it open\n  useDrag(\n    header,\n    {\n      onPress: (_, e) => {\n        if ((e.target as HTMLElement).closest(\"button:not([data-handle]), a, input, textarea, select\")) return false;\n        // a sheet still settling stops under the hand and is dragged from where it is\n        gliding.current?.stop();\n        drag.current = { from: y.get() };\n        setHeld(true);\n      },\n      onMove: (p) => {\n        const d = drag.current;\n        if (!d) return;\n        const next = d.from + p.dy;\n        y.set(next < 0 ? next * 0.2 : next); // resist past the top\n      },\n      onEnd: (p) => {\n        drag.current = null;\n        setHeld(false);\n        const v = p.vy / 1000; // px per ms\n        const projected = y.get() + v * 180;\n        if (projected > yOf(0) + (maxH - yOf(0)) * 0.5 || (v > 0.9 && detentAt(y.get()) === 0)) {\n          close();\n          return;\n        }\n        settle(detentAt(projected));\n      },\n      onTap: () => {\n        const d = drag.current;\n        drag.current = null;\n        setHeld(false);\n        if (d && Math.abs(y.get() - yOf(detent)) > 0.5) settle(detent);\n      },\n      onCancel: () => {\n        const d = drag.current;\n        drag.current = null;\n        setHeld(false);\n        if (d) settle(detentAt(d.from));\n      },\n      // a press the page took before it became a drag (a sideways move scrolls instead): let the handle go, and put a\n      // sheet stopped mid-glide on its detent\n      onAbandon: () => {\n        const d = drag.current;\n        drag.current = null;\n        setHeld(false);\n        if (d && Math.abs(y.get() - yOf(detent)) > 0.5) settle(detent);\n      },\n    },\n    { axis: \"y\" },\n  );\n  const detentAt = (pos: number) =>\n    detents.reduce((best, _, i) => (Math.abs(yOf(i) - pos) < Math.abs(yOf(best) - pos) ? i : best), 0);\n\n  return (\n    <>\n      <RadixDialog.Overlay asChild>\n        <motion.div\n          data-slot=\"bottom-sheet-backdrop\"\n          // a leaving sheet lets taps through to the page\n          className={cn(\n            overlay.scrim,\n            \"!animate-none data-[state=closed]:pointer-events-none\",\n            away && \"!opacity-0\",\n            classNames?.backdrop,\n          )}\n          style={{ opacity: scrim }}\n          onPointerDown={(e) => {\n            e.preventDefault();\n            close();\n          }}\n        />\n      </RadixDialog.Overlay>\n      <RadixDialog.Content\n        asChild\n        onEscapeKeyDown={(e) => {\n          e.preventDefault();\n          close();\n        }}\n        onPointerDownOutside={(e) => e.preventDefault()}\n        {...focus}\n      >\n        <motion.div\n          ref={attach}\n          data-slot=\"bottom-sheet\"\n          style={{ y, height: maxH, ...style }}\n          // at a lower detent part of the sheet is below the screen: a control focused down there raises the sheet\n          onFocusCapture={(e) => {\n            const last = detents.length - 1;\n            if (\n              detent < last &&\n              !drag.current &&\n              (e.target as HTMLElement).getBoundingClientRect().bottom - e.currentTarget.getBoundingClientRect().top >\n                Math.round(vh * detents[detent])\n            )\n              settle(last);\n          }}\n          className={cn(\n            \"m-overlay fixed inset-x-0 bottom-0 z-[var(--pui-z-modal)] mx-auto flex max-w-[640px] flex-col rounded-t-[var(--pui-radius-window)] text-ink outline-none data-[state=closed]:pointer-events-none\",\n            // closed by its owner it is gone from the first frame, still focusable until focus has gone back\n            away && \"opacity-0\",\n            classNames?.root,\n            className,\n          )}\n        >\n          <div\n            ref={header}\n            data-slot=\"bottom-sheet-header\"\n            data-held={held ? \"\" : undefined}\n            className={cn(\n              \"group/sheet shrink-0 cursor-grab touch-pan-x select-none active:cursor-grabbing\",\n              classNames?.header,\n            )}\n          >\n            <button // organism-allow: raw-control -- the sheet's drag handle (a grip, not an action)\n              type=\"button\"\n              data-slot=\"bottom-sheet-handle\"\n              data-handle\n              aria-label={detent === detents.length - 1 ? i18n(\"collapseSheet\") : i18n(\"expandSheet\")}\n              onClick={() => settle((detent + 1) % detents.length)}\n              onKeyDown={(e) => {\n                if (e.key === \"ArrowUp\") {\n                  e.preventDefault();\n                  settle(Math.min(detents.length - 1, detent + 1));\n                } else if (e.key === \"ArrowDown\") {\n                  e.preventDefault();\n                  if (detent === 0) close();\n                  else settle(detent - 1);\n                }\n              }}\n              className={cn(\"mx-auto mt-2 mb-1 grid h-5 w-16 place-items-center rounded-pill\", classNames?.handle)}\n            >\n              <span className=\"h-1 w-9 rounded-pill bg-line-strong transition-[width,background-color] duration-[var(--pui-motion-out)] group-data-held/sheet:w-12 group-data-held/sheet:bg-ink-3 group-data-held/sheet:duration-0\" />\n            </button>\n            <div className=\"flex items-start gap-3 px-[var(--pui-pad-panel)] pb-3\">\n              <div className=\"min-w-0 flex-1\">\n                <RadixDialog.Title\n                  data-slot=\"bottom-sheet-title\"\n                  className={cn(\"text-subheading leading-snug text-ink\", classNames?.title)}\n                >\n                  {title}\n                </RadixDialog.Title>\n                {description ? (\n                  <RadixDialog.Description\n                    data-slot=\"bottom-sheet-description\"\n                    className={cn(\"text-sm text-ink-2\", classNames?.description)}\n                  >\n                    {description}\n                  </RadixDialog.Description>\n                ) : (\n                  <RadixDialog.Description className=\"sr-only\">{title}</RadixDialog.Description>\n                )}\n              </div>\n              <Button\n                data-slot=\"bottom-sheet-close\"\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={close}\n                aria-label={closeLabel}\n                className={cn(\"-me-1\", classNames?.close)}\n              >\n                <X />\n              </Button>\n            </div>\n          </div>\n          <div\n            data-slot=\"bottom-sheet-body\"\n            className={cn(\"min-h-0 overflow-y-auto overscroll-contain border-t border-line-soft\", classNames?.body)}\n            style={{ height: Math.round(vh * detents[detent]) - headerH }}\n          >\n            {children}\n          </div>\n        </motion.div>\n      </RadixDialog.Content>\n    </>\n  );\n}\n\nexport default BottomSheet;\n"},{"path":"beamline/components/bottom-sheet/messages.ts","type":"registry:file","target":"src/ui/beamline/components/bottom-sheet/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  collapseSheet: \"Collapse sheet\",\n  expandSheet: \"Expand sheet\",\n  close: \"Close\",\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/checkbox/checkbox.module.css","type":"registry:file","target":"src/ui/beamline/components/checkbox/checkbox.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Checkbox: a 16px box painted by the toggle role (a well with a 3:1 ring that fills when checked); the check draws\n     itself in, the dash for \"some\" grows from the middle. */\n  .box {\n    --m-color: var(--pui-control-on);\n    --m-toggle-off: var(--pui-field);\n    --m-toggle-off-hover: var(--pui-field);\n    --m-toggle-ring: var(--pui-edge-control);\n    --m-toggle-ring-hover: color-mix(in oklab, var(--pui-edge-control) 60%, var(--pui-ink));\n    position: relative;\n    flex-shrink: 0;\n    display: grid;\n    place-items: center;\n    width: 16px;\n    height: 16px;\n    margin-top: calc(var(--pui-space-0-5) / 2);\n    /* a 16px box: 2px under the look's chip radius (square in blueprint) */\n    border-radius: calc(var(--pui-radius-chip) - 2px);\n    color: var(--pui-control-glyph);\n    cursor: pointer;\n    transition: var(--m-transition);\n  }\n  /* the drawn box is 16px; the area that takes a tap or click is 24px (WCAG 2.5.8), so dense rows stay easy to hit */\n  .box::after {\n    content: \"\";\n    position: absolute;\n    inset: -4px;\n  }\n  .box:hover:not(:disabled),\n  .field:hover:not([data-disabled]) .box,\n  .box[data-state=\"checked\"],\n  .box[data-state=\"indeterminate\"] {\n    transition-duration: 0ms;\n  }\n  /* the whole row is the target: pointing at the label lights the box's ring as pointing at the box does */\n  .field:hover:not([data-disabled]) .box {\n    --m-toggle-ring: var(--m-toggle-ring-hover);\n  }\n  /* a press is acknowledged at once, deeper than a button (a small thing presses deeper) */\n  .box:active:not(:disabled) {\n    scale: var(--pui-press-scale);\n  }\n  .box:disabled {\n    opacity: var(--pui-opacity-disabled);\n    cursor: not-allowed;\n  }\n  .box[aria-invalid=\"true\"] {\n    --m-toggle-ring: var(--pui-red);\n    --m-toggle-ring-hover: var(--pui-red);\n  }\n  .glyph {\n    width: 12px;\n    height: 12px;\n    fill: none;\n    stroke: currentColor;\n    stroke-width: 2.2;\n    stroke-linecap: round;\n    stroke-linejoin: round;\n  }\n  .check {\n    stroke-dasharray: 14;\n    stroke-dashoffset: 14;\n    transition: stroke-dashoffset var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .box[data-state=\"checked\"] .check {\n    stroke-dashoffset: 0;\n  }\n  .dash {\n    transform-origin: center;\n    transform: scaleX(0);\n    transition: transform var(--pui-duration-fast) var(--pui-ease-enter);\n  }\n  .box[data-state=\"indeterminate\"] .dash {\n    transform: scaleX(1);\n  }\n\n  /* labelled form: box, then the label over its description or error (icon + words, as in every field) */\n  .field {\n    display: flex;\n    align-items: flex-start;\n    gap: var(--pui-space-2-5);\n  }\n  .text {\n    --pui-field-gap: var(--pui-space-0-5);\n    display: flex;\n    flex-direction: column;\n    gap: var(--pui-field-gap);\n  }\n  .label {\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    font-weight: var(--pui-weight-medium);\n    line-height: var(--pui-leading-body);\n    cursor: pointer;\n    user-select: none;\n  }\n  .field[data-disabled] .label,\n  .field[data-disabled] [data-slot=\"checkbox-description\"] {\n    opacity: var(--pui-opacity-disabled);\n    cursor: not-allowed;\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.box {\n  composes: m-toggle from global;\n}\n"},{"path":"beamline/components/checkbox/checkbox.tsx","type":"registry:file","target":"src/ui/beamline/components/checkbox/checkbox.tsx","content":"\"use client\";\n\nimport { useId, type ComponentProps, type ReactNode } from \"react\";\nimport * as RadixCheckbox from \"@radix-ui/react-checkbox\";\nimport { FieldMessage } from \"../_shared/field\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./checkbox.module.css\";\n\nexport type CheckboxClassNames = Partial<\n  Record<\"root\" | \"control\" | \"glyph\" | \"text\" | \"label\" | \"description\" | \"error\", string>\n>;\n\nexport interface CheckboxProps extends ComponentProps<typeof RadixCheckbox.Root> {\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: CheckboxClassNames;\n  /** Visible label; clicking it toggles the box. Without it, pass aria-label. */\n  label?: ReactNode;\n  /** One line of help under the label. */\n  description?: ReactNode;\n  /** Marks the box invalid without words of its own (the message is shown elsewhere, e.g. for a group). */\n  invalid?: boolean;\n  /** The problem and its fix (\"Accept the terms to continue.\"); replaces the description and marks the box invalid. */\n  error?: ReactNode;\n}\n\n/**\n * A yes/no choice, including \"some\" (checked=\"indeterminate\") for select-all. The check draws itself when ticked;\n * checked / defaultChecked / onCheckedChange as with any Radix checkbox.\n */\nexport function Checkbox({\n  label,\n  description,\n  invalid,\n  error,\n  id,\n  className,\n  style,\n  classNames,\n  disabled,\n  ref,\n  ...props\n}: CheckboxProps) {\n  const auto = useId();\n  const boxId = id ?? `checkbox-${auto}`;\n  const descId = description && !error ? `${boxId}-description` : undefined;\n  const errorId = error ? `${boxId}-error` : undefined;\n  const box = (\n    <RadixCheckbox.Root\n      ref={ref}\n      style={!label ? style : undefined}\n      id={boxId}\n      data-slot=\"checkbox\"\n      disabled={disabled}\n      aria-describedby={[descId, errorId].filter(Boolean).join(\" \") || undefined}\n      aria-invalid={invalid || error ? true : undefined}\n      aria-errormessage={errorId}\n      className={cn(styles.box, classNames?.control, !label && classNames?.root, !label && className)}\n      {...props}\n    >\n      <svg viewBox=\"0 0 12 12\" aria-hidden data-slot=\"checkbox-glyph\" className={cn(styles.glyph, classNames?.glyph)}>\n        <path className={styles.check} d=\"M2.5 6.2 5 8.6l4.6-5\" />\n        <path className={styles.dash} d=\"M3 6h6\" />\n      </svg>\n    </RadixCheckbox.Root>\n  );\n  if (!label) return box;\n  return (\n    <div\n      data-slot=\"checkbox-field\"\n      data-disabled={disabled ? \"\" : undefined}\n      data-invalid={invalid || error ? \"\" : undefined}\n      style={style}\n      className={cn(styles.field, classNames?.root, className)}\n    >\n      {box}\n      <div data-slot=\"checkbox-text\" className={cn(styles.text, classNames?.text)}>\n        <label htmlFor={boxId} data-slot=\"checkbox-label\" className={cn(styles.label, classNames?.label)}>\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=\"checkbox\"\n          classNames={{ error: classNames?.error, description: classNames?.description }}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default Checkbox;\n"},{"path":"beamline/components/chip-group/chip-group.module.css","type":"registry:file","target":"src/ui/beamline/components/chip-group/chip-group.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Chips: raised controls when off, ink solids when on; a check opens in front of the label of a chosen chip. */\n  .group {\n    display: flex;\n    flex-wrap: wrap;\n    gap: var(--pui-space-1-5);\n  }\n  .chip {\n    --m-color: var(--pui-ink);\n    display: inline-flex;\n    height: 28px;\n    align-items: center;\n    gap: var(--pui-space-1);\n    padding-inline: var(--pui-space-3);\n    border-radius: var(--pui-radius-pill);\n    color: var(--pui-ink-2);\n    font-size: var(--pui-text-sm);\n    transition:\n      var(--m-transition),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      padding var(--pui-duration-fast) var(--pui-ease-default),\n      scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .chip:hover {\n    color: var(--pui-ink);\n    transition: none;\n  }\n  /* a press is acknowledged at 0 ms and released over --pui-motion-out; the fill and the check then follow the state */\n  .chip:active {\n    scale: var(--pui-press-scale);\n    transition: none;\n  }\n  .chip[aria-pressed=\"true\"],\n  .chip[aria-checked=\"true\"] {\n    padding-inline-start: var(--pui-space-2);\n    color: var(--pui-ink-inverse);\n  }\n  .chip:disabled {\n    opacity: var(--pui-opacity-disabled);\n    pointer-events: none;\n  }\n  .check {\n    display: grid;\n    width: 0;\n    overflow: hidden;\n    opacity: 0;\n    transition:\n      width var(--pui-duration-fast) var(--pui-ease-default),\n      opacity var(--pui-duration-fast) var(--pui-ease-default);\n  }\n  .check :where(svg) {\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n  }\n  .chip[aria-pressed=\"true\"] .check,\n  .chip[aria-checked=\"true\"] .check {\n    width: var(--pui-icon-sm);\n    opacity: 1;\n  }\n  /* an option's mark: its colour as a dot, or its icon in that colour; a faint ring in the chip's own ink keeps a pale\n     or a dark dot visible on the raised chip and on the filled one alike */\n  .mark {\n    display: grid;\n    flex: none;\n    place-items: center;\n    color: var(--_mark, currentColor);\n  }\n  .mark[data-kind=\"dot\"] {\n    width: 8px;\n    height: 8px;\n    border-radius: 50%;\n    background: var(--_mark);\n    color: inherit;\n    box-shadow: 0 0 0 1px color-mix(in oklab, currentColor 30%, transparent);\n  }\n  /* on a chosen chip the fill can share the dot's hue or depth (ink's blue, metal's black): a solid ring in the chip's\n     text colour sets the dot apart from any fill */\n  .chip[aria-pressed=\"true\"] .mark[data-kind=\"dot\"],\n  .chip[aria-checked=\"true\"] .mark[data-kind=\"dot\"] {\n    box-shadow: 0 0 0 1.5px currentColor;\n  }\n  /* on a chosen chip's ink fill an icon takes the chip's ink (a pale or dark colour there could vanish); the dot keeps\n     its colour on both, so the key still reads */\n  .chip[aria-pressed=\"true\"] .mark[data-kind=\"icon\"],\n  .chip[aria-checked=\"true\"] .mark[data-kind=\"icon\"] {\n    color: inherit;\n  }\n  .mark :where(svg) {\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n  }\n  .count {\n    color: var(--pui-ink-3);\n    font-family: var(--pui-font-numeric);\n    font-size: var(--pui-text-2xs);\n    font-variant-numeric: tabular-nums;\n  }\n  .chip[aria-pressed=\"true\"] .count,\n  .chip[aria-checked=\"true\"] .count {\n    color: color-mix(in oklab, var(--pui-ink-inverse) 70%, transparent);\n  }\n  /* \"+3 more\" and \"Show less\": quiet text buttons in the chip row */\n  .more {\n    display: inline-flex;\n    height: 28px;\n    align-items: center;\n    padding-inline: var(--pui-space-3);\n    border-radius: var(--pui-radius-pill);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-sm);\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .more:hover {\n    background: var(--pui-hover);\n    color: var(--pui-ink);\n    transition: none;\n  }\n  .more:active {\n    scale: var(--pui-press-scale);\n    transition: none;\n  }\n}\n"},{"path":"beamline/components/chip-group/chip-group.tsx","type":"registry:file","target":"src/ui/beamline/components/chip-group/chip-group.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 type { CSSProperties, ReactNode, Ref } from \"react\";\n\nimport { useRef, useState, type KeyboardEvent } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useFit } from \"../../hooks/use-fit\";\nimport { useFlip } from \"../../hooks/use-flip\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { ChoiceField, choiceIsField, type ChoiceFieldAria } from \"../_shared/field\";\nimport { FoldMore } from \"../_shared/fold-more\";\nimport styles from \"./chip-group.module.css\";\n\nexport interface ChipOption {\n  /** Value represented by this item. Required. */\n  value: string;\n  /** Text displayed for this item. Required. */\n  label: string;\n  /** A count after the label: how many results it would show. */ count?: number;\n  /** A colour that names the thing this chip stands for, drawn as a dot before the label (or as the icon's colour), so\n   *  the chips act as the colour key for a calendar, chart or map that paints the same things: take it from\n   *  `identity(i, n)` in `@/lib/tone`, the same call that colours the events or slices. Omitted by default: no dot. */\n  color?: string;\n  /** An icon before the label (a channel's or account's mark), tinted by `color` when one is given (in the chip's ink\n   *  once chosen). Omitted by default. */\n  icon?: ReactNode;\n  /** Whether this option is unavailable for selection. Omitted by default. */\n  disabled?: boolean;\n}\n\nexport interface ChipGroupClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the chip part; no override by default. */\n  chip?: string;\n  /** Classes for the check part; no override by default. */\n  check?: string;\n  /** Classes for the count part; no override by default. */\n  count?: string;\n  /** Classes for the mark part (an option's colour dot or icon); no override by default. */\n  mark?: string;\n  /** Classes for the more part; no override by default. */\n  more?: string;\n  /** As a form field: the wrapper, its label and its message. */\n  field?: string;\n  /** Classes for the label part; no override by default. */\n  label?: string;\n  /** Classes for the description part; no override by default. */\n  description?: string;\n  /** Classes for the error part; no override by default. */\n  error?: string;\n}\n\nexport interface ChipGroupProps {\n  /** Available choices with their labels and values. Required. */\n  options: ChipOption[];\n  /** Controlled option values selected in the group; omit to use defaultValue. */\n  value?: string[];\n  /** Initial option values selected in the group when uncontrolled. Defaults to an empty list. */\n  defaultValue?: string[];\n  /** Reports option values selected in the group after an edit; no callback by default. */\n  onValueChange?: (value: string[]) => void;\n  /** The group's name or question (\"Segments\", \"Interests\"), shown above it like every field's label; `hideLabel` keeps it for\n   *  assistive tech only (a quick filter). */\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 chips (makes it a field). */\n  description?: ReactNode;\n  /** The problem and its fix, e.g. \"Pick at least one\" (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  /** Several can be on (toggle buttons); otherwise one (a radio group). */\n  multiple?: boolean;\n  /** Show the first this many chips, then a \"+N\" chip; a chosen chip always shows wherever it is. */\n  maxVisible?: number;\n  /** Lines the chips may take before the ones that do not fit fold into \"+N\" (they come back as the room grows).\n   *  Default 2 for a quick filter, off (wrap every chip) for a form field; `false` turns it off. */\n  lines?: number | false;\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?: ChipGroupClassNames;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * Quick filters as chips. A chosen chip fills and grows a check; with `multiple` each chip toggles on its own,\n * otherwise picking one releases the other. Arrows move between chips. An option's `color` or `icon` puts a mark before\n * its label, so the chips can be the colour key for what they filter.\n */\nexport function ChipGroup({\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  multiple = false,\n  maxVisible,\n  lines: linesProp,\n  className,\n  style,\n  classNames,\n  ref,\n}: ChipGroupProps) {\n  const i18n = useMessages(\"chip-group\", englishMessages);\n\n  useDeprecatedProp(\"ChipGroup\", \"hideLabel\", \"showLabel={false}\", hideLabelProp !== undefined);\n  const hideLabel = showLabel === undefined ? hideLabelProp : !showLabel;\n  const [value, set] = useControllable(valueProp, defaultValue, onValueChange);\n  const [expanded, setExpanded] = useState(false);\n  const toggle = (v: string) =>\n    set(multiple ? (value.includes(v) ? value.filter((x) => x !== v) : [...value, v]) : [v]);\n  const asField = choiceIsField(field, label, hideLabel, description != null || error != null || !!required);\n  const lines = linesProp === undefined ? (asField ? false : 2) : linesProp;\n  const row = useRef<HTMLDivElement>(null);\n  const rowRef = useComposedRef(row, ref);\n  // the room decides how many show; chips that come back grow in and the \"+N\" slides along\n  const keep = options.map((o) => value.includes(o.value));\n  // a stable key: the same chips built afresh on each render (an inline array) are not a new set\n  const chipSet = options\n    .map((o) => `${o.value}\\u0000${o.label}\\u0000${o.count ?? \"\"}\\u0000${o.color ?? \"\"}\\u0000${o.icon ? 1 : 0}`)\n    .join(\"\\u0001\");\n  const fits = useFit(row, { items: options.length, lines: lines || 1, enabled: !!lines && !expanded, keep }, chipSet);\n  useFlip(row, { ghost: false });\n  // maxVisible counts places, as it always has: the first N chips, and a chosen chip wherever it is; of the others in\n  // those places, the room shows the first that fit (order never changes)\n  const cap = expanded || maxVisible == null ? options.length : maxVisible;\n  const room = expanded ? options.length : fits;\n  let rank = 0;\n  const shown = options.filter((o, i) => value.includes(o.value) || (i < cap && rank++ < room));\n  const hidden = options.length - shown.length;\n  const onKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    const dir = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[e.key];\n    const end = e.key === \"Home\" ? 0 : e.key === \"End\" ? -1 : null;\n    if (!dir && end === null) return;\n    const chips = [...e.currentTarget.querySelectorAll<HTMLButtonElement>(\"button:not(:disabled)\")];\n    const i = chips.indexOf(document.activeElement as HTMLButtonElement);\n    if (i < 0) return;\n    e.preventDefault();\n    // Home and End go to the first and last chip (the \"+N\" is the last stop in a folded row)\n    const next =\n      end !== null\n        ? chips.at(end)!\n        : chips[\n            (i +\n              (getComputedStyle(e.currentTarget).direction === \"rtl\" &&\n              (e.key === \"ArrowLeft\" || e.key === \"ArrowRight\")\n                ? -dir!\n                : dir!) +\n              chips.length) %\n              chips.length\n          ];\n    next.focus();\n    if (!multiple && next.dataset.value) toggle(next.dataset.value);\n  };\n  const control = (aria?: ChoiceFieldAria) => (\n    <div\n      role={multiple ? \"group\" : \"radiogroup\"}\n      aria-label={aria ? undefined : label}\n      {...aria}\n      ref={rowRef}\n      style={asField ? undefined : style}\n      data-slot=\"chip-group\"\n      onKeyDown={onKey}\n      className={cn(styles.group, classNames?.root, !asField && className)}\n    >\n      {shown.map((o) => {\n        const on = value.includes(o.value);\n        return (\n          <button\n            key={o.value}\n            type=\"button\"\n            data-value={o.value}\n            data-fit-item\n            data-flip={o.value}\n            disabled={o.disabled}\n            {...(multiple\n              ? { \"aria-pressed\": on }\n              : { role: \"radio\", \"aria-checked\": on, tabIndex: on || (!value.length && o === shown[0]) ? 0 : -1 })}\n            onClick={() => toggle(o.value)}\n            data-slot=\"chip-group-chip\"\n            className={cn(styles.chip, on ? \"m-solid\" : \"m-control\", classNames?.chip)}\n          >\n            <span aria-hidden className={cn(styles.check, classNames?.check)} data-slot=\"chip-group-check\">\n              <Check strokeWidth={2.5} />\n            </span>\n            {(o.color || o.icon) && (\n              <span\n                aria-hidden\n                data-slot=\"chip-group-mark\"\n                data-kind={o.icon ? \"icon\" : \"dot\"}\n                className={cn(styles.mark, classNames?.mark)}\n                style={o.color ? ({ \"--_mark\": o.color } as CSSProperties) : undefined}\n              >\n                {o.icon}\n              </span>\n            )}\n            {o.label}\n            {o.count != null && (\n              <span className={cn(styles.count, classNames?.count)} data-slot=\"chip-group-count\">\n                {o.count}\n              </span>\n            )}\n          </button>\n        );\n      })}\n      {hidden > 0 && (\n        <FoldMore\n          names={options.filter((o) => !shown.includes(o)).map((o) => o.label)}\n          onExpand={() => setExpanded(true)}\n          slot=\"chip-group-more\"\n          className={cn(\"h-7 rounded-pill px-3 text-sm\", classNames?.more)}\n        />\n      )}\n      {expanded && (\n        <button\n          type=\"button\"\n          onClick={() => setExpanded(false)}\n          className={cn(styles.more, classNames?.more)}\n          data-slot=\"chip-group-more\"\n        >\n          {i18n(\"showFewer\")}\n        </button>\n      )}\n    </div>\n  );\n  if (!asField) return control();\n  return (\n    <ChoiceField\n      slot=\"chip-group-field\"\n      label={label}\n      hideLabel={hideLabel}\n      required={required}\n      description={description}\n      error={error}\n      className={cn(classNames?.field, className)}\n      style={style}\n      classNames={{ label: classNames?.label, description: classNames?.description, error: classNames?.error }}\n    >\n      {control}\n    </ChoiceField>\n  );\n}\n\nexport default ChipGroup;\n"},{"path":"beamline/components/chip-group/messages.ts","type":"registry:file","target":"src/ui/beamline/components/chip-group/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  showFewer: \"Show fewer\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/combobox/combobox.tsx","type":"registry:file","target":"src/ui/beamline/components/combobox/combobox.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { ComboboxDefaults } from \"./defaults\";\nexport type { ComboboxDefaults } from \"./defaults\";\n\nimport { useEffect, useId, useMemo, useRef, useState, type Ref, type CSSProperties, type ReactNode } from \"react\";\nimport { ChevronsUpDown, Plus, Search, X } from \"lucide-react\";\nimport * as RadixPopover from \"@radix-ui/react-popover\";\nimport {\n  Field,\n  FieldFrame,\n  fieldControlClass,\n  useFieldIds,\n  type FieldClassNames,\n  type FieldSize,\n} from \"../_shared/field\";\nimport { Listbox, useListbox, type ListOption } from \"../_shared/listbox\";\nimport { BottomSheet } from \"../bottom-sheet/bottom-sheet\";\nimport { useMediaQuery } from \"../../hooks/use-media-query\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { IconAction } from \"../_shared/icon-action\";\nimport overlay from \"../_shared/overlay.module.css\";\n\nexport type ComboboxOption = ListOption;\n\n/** The create row's value: never a real option's. */\nconst CREATE = \"\\u0000create\";\n/** Rows a Page key moves. */\nconst PAGE = 10;\n\nexport interface ComboboxProps {\n  label: ReactNode;\n  hideLabel?: boolean;\n  options: ComboboxOption[];\n  value?: string | null;\n  defaultValue?: string | null;\n  onValueChange?: (value: string) => void;\n  description?: ReactNode;\n  error?: ReactNode;\n  placeholder?: string;\n  /** What to say when nothing matches. Default: No match for “…”. */\n  emptyMessage?: ReactNode;\n  /** Show a × that clears the choice. It shows while the field is hovered or holds focus (always on touch). */\n  clearable?: boolean;\n  /** Offer to make a new choice from a search that matches no option exactly: a last row, Create “…”. Resolve with the\n   *  new option (it is chosen, and stays in the list even before `options` carries it) or with an error, said under the\n   *  field with the search kept. Without it the list offers only `options`, as before. */\n  onCreate?: (query: string) => Promise<ComboboxOption | { error: string }>;\n  /** The create row's words. Default: Create “…”. */\n  createLabel?: (query: string) => string;\n  /** Heading of the list's bottom sheet below 640 px. Default: the label. */\n  sheetTitle?: string;\n  size?: FieldSize;\n  disabled?: boolean;\n  required?: boolean;\n  id?: string;\n  className?: string;\n  /** Classes for documented field parts; root style does not replace native behavior. */\n  classNames?: FieldClassNames & Partial<Record<\"input\" | \"clear\" | \"toggle\" | \"content\", string>>;\n  style?: CSSProperties;\n  ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * One value from a long list, found by typing: time zones, countries, customers. Typing ranks label starts first;\n * ↑ ↓ move, Page Up / Down by ten, Enter picks, Escape closes and restores what was chosen. The field keeps focus\n * throughout; a list of thousands mounts only the rows in view. Below 640 px\n * the list opens in a bottom sheet with its own search field, clear of the on-screen keyboard; focus comes back to\n * the field when it closes.\n */\nexport function Combobox({\n  label,\n  hideLabel,\n  options,\n  value: valueProp,\n  defaultValue = null,\n  onValueChange,\n  description,\n  error,\n  placeholder: placeholderProp,\n  emptyMessage,\n  clearable,\n  onCreate,\n  createLabel,\n  sheetTitle,\n  size: sizeProp,\n  disabled,\n  required,\n  id,\n  className,\n  classNames,\n  style,\n  ref,\n}: ComboboxProps) {\n  const i18n = useMessages(\"combobox\", englishMessages);\n  const placeholder = placeholderProp === undefined ? i18n(\"placeholder\") : placeholderProp;\n\n  const scopeDefaults = useComponentDefaults<ComboboxDefaults>(\"combobox\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  // the value is null once cleared, while onValueChange reports a clear as \"\" (its documented contract)\n  const [value, setValue] = useControllable<string | null>(valueProp, defaultValue);\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const [typing, setTyping] = useState(false);\n  // choices made here stay listed until `options` carries them itself\n  const [created, setCreated] = useState<ComboboxOption[]>([]);\n  const [creating, setCreating] = useState<string | null>(null);\n  const [createError, setCreateError] = useState<string | null>(null);\n  const all = useMemo(\n    () =>\n      created.length ? [...options, ...created.filter((c) => !options.some((o) => o.value === c.value))] : options,\n    [options, created],\n  );\n  const chosen = all.find((o) => o.value === value) ?? null;\n  const asked = query.trim();\n  const createRow = useMemo<ComboboxOption | null>(\n    () =>\n      onCreate && typing && asked && !all.some((o) => o.label.trim().toLowerCase() === asked.toLowerCase())\n        ? {\n            value: CREATE,\n            label:\n              creating === asked\n                ? i18n(\"creating\", { query: asked })\n                : (createLabel?.(asked) ?? i18n(\"create\", { query: asked })),\n            icon: <Plus aria-hidden className=\"size-4 text-ink-3\" />,\n            keywords: [asked],\n            disabled: creating !== null,\n          }\n        : null,\n    [onCreate, typing, asked, all, creating, createLabel, i18n],\n  );\n  // The list keeps its search through the exit; resetting it while closing resizes the observed popover.\n  const { filtered, active, setActive, move, jump, viaKeys, current } = useListbox(all, query, createRow);\n  const { controlId, descriptionId, errorId, aria } = useFieldIds(id, {\n    description,\n    error: error ?? createError ?? undefined,\n  });\n  const listId = useId();\n  const input = useRef<HTMLInputElement>(null);\n  const attach = useComposedRef(input, ref);\n  const container = usePortalContainer();\n  // a phone: the list is a sheet, and the field only opens it (no keyboard under a popover)\n  const phone = useMediaQuery(\"(max-width: 639px)\");\n  const [sheet, setSheet] = useState(false);\n  // a sheet left open when the screen widens past 640 px must not come back by itself\n  useEffect(() => {\n    if (!phone) setSheet(false);\n  }, [phone]);\n  const openSheet = (initial = \"\") => {\n    if (disabled) return;\n    setQuery(initial);\n    setTyping(!!initial);\n    setSheet(true);\n  };\n\n  // when the list opens without typing, the chosen row is the active one\n  useEffect(() => {\n    if ((open || sheet) && !typing && chosen)\n      setActive(\n        Math.max(\n          0,\n          filtered.findIndex((o) => o.value === chosen.value),\n        ),\n      );\n  }, [open, sheet]); // eslint-disable-line react-hooks/exhaustive-deps -- on open only\n  const pick = (o: ListOption) => {\n    if (o.value === CREATE) {\n      void create(asked);\n      return;\n    }\n    setCreateError(null);\n    setValue(o.value);\n    onValueChange?.(o.value);\n    setTyping(false);\n    setOpen(false);\n    setSheet(false);\n  };\n  const create = async (text: string) => {\n    if (!onCreate || creating) return;\n    setCreating(text);\n    setCreateError(null);\n    const r = await onCreate(text);\n    setCreating(null);\n    if (\"error\" in r) {\n      setCreateError(i18n(\"createFailed\", { query: text, reason: r.error }));\n      return;\n    }\n    setCreated((c) => [...c.filter((x) => x.value !== r.value), r]);\n    pick(r);\n  };\n  const close = () => {\n    setOpen(false);\n    setTyping(false);\n  };\n  const openList = () => {\n    setQuery(\"\");\n    setTyping(false);\n    setOpen(true);\n  };\n\n  return (\n    <Field\n      label={label}\n      hideLabel={hideLabel}\n      description={description}\n      error={error ?? createError ?? undefined}\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      <RadixPopover.Root\n        open={open && !disabled}\n        onOpenChange={(o) => {\n          if (!o) close();\n        }}\n      >\n        <RadixPopover.Anchor asChild>\n          <div className={clearable ? \"reveal-host\" : undefined}>\n            <FieldFrame\n              classNames={classNames}\n              size={size}\n              invalid={!!error || !!createError}\n              disabled={disabled}\n              trailing={\n                // nesting input (O1 §14.1): the 24px clear/open buttons round concentric with the field\n                <span className=\"flex items-center gap-[var(--pui-space-0-5)]\">\n                  {clearable && chosen && !disabled && (\n                    <IconAction\n                      data-slot=\"combobox-clear\"\n                      aria-label={i18n(\"clear\")}\n                      onMouseDown={(e) => e.preventDefault()}\n                      onClick={() => {\n                        setValue(null);\n                        onValueChange?.(\"\");\n                        input.current?.focus();\n                      }}\n                      className={cn(\"reveal\", classNames?.clear)}\n                    >\n                      <X className=\"size-3.5\" />\n                    </IconAction>\n                  )}\n                  <IconAction\n                    tabIndex={-1}\n                    data-slot=\"combobox-toggle\"\n                    aria-label={open || sheet ? i18n(\"closeList\") : i18n(\"openList\")}\n                    disabled={disabled}\n                    onMouseDown={(e) => e.preventDefault()}\n                    onClick={() => {\n                      if (phone) openSheet();\n                      else if (open) close();\n                      else openList();\n                      input.current?.focus();\n                    }}\n                    pressed={open}\n                    className={classNames?.toggle}\n                  >\n                    <ChevronsUpDown className=\"size-4\" />\n                  </IconAction>\n                </span>\n              }\n            >\n              <input\n                ref={attach}\n                data-slot=\"combobox\"\n                {...aria}\n                role=\"combobox\"\n                aria-expanded={phone ? sheet : open}\n                aria-haspopup={phone ? \"dialog\" : undefined}\n                aria-controls={phone ? undefined : listId}\n                aria-autocomplete={phone ? undefined : \"list\"}\n                required={required}\n                aria-activedescendant={!phone && open && current ? `${listId}-${active}` : undefined}\n                disabled={disabled}\n                // no on-screen keyboard under the field on a phone (the sheet has its own search); unlike readOnly it still\n                // takes part in required validation\n                inputMode={phone ? \"none\" : undefined}\n                autoComplete=\"off\"\n                spellCheck={false}\n                placeholder={placeholder}\n                // a value missing from the options (a time zone outside the list) still shows, not the placeholder\n                value={!phone && typing ? query : (chosen?.label ?? value ?? \"\")}\n                className={cn(fieldControlClass, phone && \"cursor-pointer\", classNames?.input)}\n                onChange={(e) => {\n                  if (phone) {\n                    openSheet(e.target.value);\n                    return;\n                  }\n                  setQuery(e.target.value);\n                  setTyping(true);\n                  setOpen(true);\n                  setCreateError(null);\n                }}\n                onFocus={(e) => {\n                  if (!phone) e.currentTarget.select();\n                }}\n                // a click shows the options (Tab alone does not, so moving through a form stays quiet)\n                onClick={() => {\n                  if (phone) openSheet();\n                  else if (!open && !disabled) openList();\n                }}\n                onBlur={() => {\n                  if (!phone) close();\n                }}\n                onKeyDown={(e) => {\n                  if (phone) {\n                    // the field opens the sheet; a typed letter starts its search\n                    if ([\"ArrowDown\", \"ArrowUp\", \"Enter\", \" \"].includes(e.key)) {\n                      e.preventDefault();\n                      openSheet();\n                    } else if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {\n                      e.preventDefault();\n                      openSheet(e.key);\n                    }\n                    return;\n                  }\n                  if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n                    e.preventDefault();\n                    if (!open) openList();\n                    else move(e.key === \"ArrowDown\" ? 1 : -1);\n                  } else if (e.key === \"Enter\" && open) {\n                    e.preventDefault();\n                    if (current && !current.disabled) pick(current);\n                  } else if (e.key === \"Escape\" && open) {\n                    e.preventDefault();\n                    e.stopPropagation();\n                    close();\n                  } else if (e.key === \"Home\" && open && !typing) {\n                    e.preventDefault();\n                    jump(0);\n                  } else if (e.key === \"End\" && open && !typing) {\n                    e.preventDefault();\n                    jump(filtered.length - 1);\n                  } else if ((e.key === \"PageDown\" || e.key === \"PageUp\") && open) {\n                    e.preventDefault();\n                    jump(active + (e.key === \"PageDown\" ? PAGE : -PAGE));\n                  }\n                }}\n              />\n            </FieldFrame>\n          </div>\n        </RadixPopover.Anchor>\n        {!phone && container !== null && (\n          <RadixPopover.Portal container={container}>\n            <RadixPopover.Content\n              data-slot=\"combobox-list\"\n              role=\"presentation\"\n              sideOffset={6}\n              align=\"start\"\n              collisionPadding={8}\n              onOpenAutoFocus={(e) => e.preventDefault()}\n              onCloseAutoFocus={(e) => e.preventDefault()}\n              onInteractOutside={(e) => {\n                if (e.target instanceof Node && input.current?.closest(\"[data-slot=field]\")?.contains(e.target))\n                  e.preventDefault();\n              }}\n              className={cn(\n                overlay.surface,\n                \"w-[var(--radix-popover-trigger-width)] min-w-[220px]\",\n                classNames?.content,\n              )}\n            >\n              <Listbox\n                id={listId}\n                label={typeof label === \"string\" ? label : i18n(\"options\")}\n                options={filtered}\n                query={query}\n                active={active}\n                onActive={setActive}\n                viaKeys={viaKeys}\n                isSelected={(v) => v === value}\n                onPick={pick}\n                emptyText={emptyMessage}\n              />\n            </RadixPopover.Content>\n          </RadixPopover.Portal>\n        )}\n      </RadixPopover.Root>\n      {phone && (\n        <BottomSheet\n          open={sheet && !disabled}\n          onOpenChange={(o) => {\n            if (!o) close();\n            setSheet(o);\n          }}\n          title={sheetTitle ?? (typeof label === \"string\" ? label : placeholder)}\n          detents={[0.92]}\n          classNames={{ body: \"flex flex-col\" }}\n        >\n          <SheetList\n            listId={listId}\n            label={typeof label === \"string\" ? label : i18n(\"options\")}\n            placeholder={placeholder}\n            query={query}\n            onQuery={(q) => {\n              setQuery(q);\n              setTyping(true);\n              setCreateError(null);\n            }}\n            filtered={filtered}\n            active={active}\n            setActive={setActive}\n            move={move}\n            jump={jump}\n            viaKeys={viaKeys}\n            current={current}\n            value={value}\n            onPick={pick}\n            emptyText={emptyMessage}\n            className={classNames?.content}\n          />\n        </BottomSheet>\n      )}\n    </Field>\n  );\n}\n\n/** The sheet's search field and list: the same listbox, keys and ranking as the popover, with the field inside. */\nfunction SheetList({\n  listId,\n  label,\n  placeholder,\n  query,\n  onQuery,\n  filtered,\n  active,\n  setActive,\n  move,\n  jump,\n  viaKeys,\n  current,\n  value,\n  onPick,\n  emptyText,\n  className,\n}: {\n  listId: string;\n  label: string;\n  placeholder: string;\n  query: string;\n  onQuery: (query: string) => void;\n  filtered: ListOption[];\n  active: number;\n  setActive: (i: number) => void;\n  move: (delta: number) => void;\n  jump: (to: number) => void;\n  viaKeys: { current: boolean };\n  current: ListOption | undefined;\n  value: string | null;\n  onPick: (option: ListOption) => void;\n  emptyText?: ReactNode;\n  className?: string;\n}) {\n  const i18n = useMessages(\"combobox\", englishMessages);\n\n  const search = useRef<HTMLInputElement>(null);\n  // after the sheet's own focus scope settles (it records the field to return to), the search takes focus\n  useEffect(() => {\n    const frame = requestAnimationFrame(() => search.current?.focus({ preventScroll: true }));\n    return () => cancelAnimationFrame(frame);\n  }, []);\n  return (\n    <div data-slot=\"combobox-sheet\" className={cn(\"flex min-h-0 flex-1 flex-col\", className)}>\n      <div className=\"shrink-0 p-[var(--pui-space-3)] pb-[var(--pui-space-2)]\">\n        <FieldFrame size=\"lg\" leading={<Search aria-hidden className=\"size-4\" />}>\n          <input\n            ref={search}\n            data-slot=\"combobox-search\"\n            role=\"combobox\"\n            aria-label={i18n(\"search\", { label: label })}\n            aria-expanded\n            aria-controls={listId}\n            aria-autocomplete=\"list\"\n            aria-activedescendant={current ? `${listId}-${active}` : undefined}\n            autoComplete=\"off\"\n            spellCheck={false}\n            enterKeyHint=\"done\"\n            placeholder={placeholder}\n            value={query}\n            className={fieldControlClass}\n            onChange={(e) => onQuery(e.target.value)}\n            onKeyDown={(e) => {\n              if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n                e.preventDefault();\n                move(e.key === \"ArrowDown\" ? 1 : -1);\n              } else if (e.key === \"PageDown\" || e.key === \"PageUp\") {\n                e.preventDefault();\n                jump(active + (e.key === \"PageDown\" ? PAGE : -PAGE));\n              } else if (e.key === \"Enter\") {\n                e.preventDefault();\n                if (current && !current.disabled) onPick(current);\n              }\n            }}\n          />\n        </FieldFrame>\n      </div>\n      <Listbox\n        id={listId}\n        label={label}\n        options={filtered}\n        query={query}\n        active={active}\n        onActive={setActive}\n        viaKeys={viaKeys}\n        isSelected={(v) => v === value}\n        onPick={onPick}\n        emptyText={emptyText}\n        className=\"max-h-none min-h-0 flex-1 px-[var(--pui-space-2)] pb-[var(--pui-space-4)]\"\n      />\n    </div>\n  );\n}\n\nexport default Combobox;\n"},{"path":"beamline/components/combobox/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/combobox/defaults.ts","content":"/** Pure allowed presentation defaults for Combobox; no component or runtime imports. */\nexport interface ComboboxDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/combobox/messages.ts","type":"registry:file","target":"src/ui/beamline/components/combobox/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: \"Search\",\n  clear: \"Clear\",\n  closeList: \"Close list\",\n  openList: \"Open list\",\n  options: \"Options\",\n  search: \"Search {label}\",\n  create: \"Create “{query}”\",\n  creating: \"Creating “{query}”…\",\n  createFailed: \"“{query}” wasn't created: {reason}\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/copy-button/copy-button.tsx","type":"registry:file","target":"src/ui/beamline/components/copy-button/copy-button.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { CopyButtonDefaults } from \"./defaults\";\nexport type { CopyButtonDefaults } from \"./defaults\";\n\nimport { useEffect, useRef, useState, type CSSProperties, type Ref } from \"react\";\nimport { Check, Copy, X } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { Button, type ButtonSize, type ButtonVariant } from \"../button/button\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { copyText } from \"../../lib/clipboard\";\nimport { useMessages, type MessageLabels } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface CopyButtonClassNames {\n  root?: string;\n  label?: string;\n  loader?: string;\n  iconFrame?: string;\n  icon?: string;\n  status?: string;\n}\n\nexport interface CopyButtonProps {\n  /** The text that lands on the clipboard. */\n  value: string;\n  /** What is copied, e.g. \"Copy API key\"; the accessible name when icon-only. Default \"Copy\". */\n  label?: string;\n  /** Per-instance copy; overrides UIRoot messages. */\n  labels?: MessageLabels<typeof english>;\n  /** The confirmation in your words, shown in place of the label and said to a screen reader: \"Link copied\". Default\n   *  \"Copied\" (heard as \"Copied to clipboard\"). */\n  copiedLabel?: string;\n  iconOnly?: boolean;\n  /** Any Button variant; \"plain\" is kept as an alias of ghost. Default secondary. */\n  variant?: ButtonVariant | \"plain\";\n  size?: ButtonSize;\n  disabled?: boolean;\n  /** Runs after a successful copy. */\n  onCopied?: () => void;\n  /** How long \"Copied\" stays, ms. Default 1600. */\n  timeout?: number;\n  className?: string;\n  classNames?: CopyButtonClassNames;\n  style?: CSSProperties;\n  ref?: Ref<HTMLButtonElement>;\n}\n\nconst ICONS = { idle: Copy, copied: Check, failed: X } as const;\n\n/**\n * Copies a value and confirms in place: the icon turns into a check and the label into \"Copied\" for a moment, and a\n * screen reader hears it. If the browser refuses, it says so instead of pretending.\n */\nexport function CopyButton({\n  value,\n  label,\n  labels,\n  copiedLabel,\n  iconOnly = false,\n  variant: variantProp,\n  size: sizeProp,\n  disabled,\n  onCopied,\n  timeout = 1600,\n  className,\n  classNames,\n  style,\n  ref,\n}: CopyButtonProps) {\n  const t = useMessages(\"copy-button\", english, labels);\n  const scopeDefaults = useComponentDefaults<CopyButtonDefaults>(\"copy-button\", [\"variant\", \"size\"]);\n  const variant = variantProp ?? scopeDefaults.variant ?? \"secondary\";\n  const size = sizeProp ?? scopeDefaults.size ?? \"sm\";\n\n  const [state, setState] = useState<\"idle\" | \"copied\" | \"failed\">(\"idle\");\n  const timer = useRef<ReturnType<typeof setTimeout>>(undefined);\n  const host = useRef<HTMLButtonElement>(null);\n  const attach = useComposedRef(host, ref);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = tokens.reduced || managedReduced;\n  useEffect(() => () => clearTimeout(timer.current), []);\n  const run = async () => {\n    const ok = await copyText(value);\n    setState(ok ? \"copied\" : \"failed\");\n    if (ok) onCopied?.();\n    clearTimeout(timer.current);\n    timer.current = setTimeout(() => setState(\"idle\"), ok ? timeout : timeout + 1200);\n  };\n  const Icon = ICONS[state];\n  const text =\n    state === \"copied\" ? (copiedLabel ?? t(\"copied\")) : state === \"failed\" ? t(\"error\") : (label ?? t(\"label\"));\n  return (\n    <Button\n      ref={attach}\n      style={style}\n      classNames={classNames}\n      data-slot=\"copy-button\"\n      data-state={state}\n      // a result is never hidden while it shows: a copy button that waits for attention (.reveal) stays until it rests\n      data-shown={state !== \"idle\" ? \"\" : undefined}\n      variant={variant === \"plain\" ? \"ghost\" : variant}\n      iconOnly={iconOnly}\n      size={size}\n      disabled={disabled}\n      onClick={run}\n      aria-label={iconOnly ? text : undefined}\n      className={cn(state === \"copied\" && \"[&_svg]:text-green\", state === \"failed\" && \"[&_svg]:text-red\", className)}\n    >\n      <span\n        data-slot=\"copy-button-icon-frame\"\n        className={cn(\"relative grid size-4 place-items-center\", classNames?.iconFrame)}\n        aria-hidden\n      >\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={state}\n            data-slot=\"copy-button-icon\"\n            className={cn(\"grid place-items-center\", classNames?.icon)}\n            initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.5, filter: \"blur(2px)\" }}\n            animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n            exit={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.5, filter: \"blur(2px)\" }}\n            transition={{ duration: reduced ? 0 : tokens.duration.feedback }}\n          >\n            <Icon className=\"size-4\" />\n          </motion.span>\n        </AnimatePresence>\n      </span>\n      {!iconOnly && text}\n      <span data-slot=\"copy-button-status\" className={cn(\"sr-only\", classNames?.status)} role=\"status\">\n        {state === \"copied\" ? (copiedLabel ?? t(\"copiedDescription\")) : state === \"failed\" ? t(\"errorDescription\") : \"\"}\n      </span>\n    </Button>\n  );\n}\n\nexport default CopyButton;\n"},{"path":"beamline/components/copy-button/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/copy-button/defaults.ts","content":"/** Pure allowed presentation defaults for CopyButton; no component or runtime imports. */\nexport interface CopyButtonDefaults {\n  variant?: \"primary\" | \"secondary\" | \"outline\" | \"ghost\" | \"danger\" | \"accent\" | \"plain\";\n  size?: \"sm\" | \"md\" | \"lg\" | \"touch\";\n}\n"},{"path":"beamline/components/copy-button/messages.ts","type":"registry:file","target":"src/ui/beamline/components/copy-button/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Copy-button English defaults. Instance labels override these namespaced messages. */\nexport const english = {\n  label: \"Copy\",\n  copied: \"Copied\",\n  error: \"Copy failed\",\n  copiedDescription: \"Copied to clipboard\",\n  errorDescription: \"Could not copy; select the text and copy it yourself\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/dialog/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/dialog/defaults.ts","content":"export interface DialogDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n}\n"},{"path":"beamline/components/dialog/dialog.tsx","type":"registry:file","target":"src/ui/beamline/components/dialog/dialog.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\nimport { useRef, type ComponentProps, type CSSProperties, type ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { Button } from \"../button/button\";\nimport * as RadixDialog from \"@radix-ui/react-dialog\";\nimport { useReturnFocus } from \"../_shared/use-return-focus\";\nimport { useOpenFrom } from \"../_shared/use-open-from\";\nimport { useHeightMorph } from \"../../hooks/use-height-morph\";\nimport { usePortalContainer, useComponentDefaults } from \"../../runtime/appearance\";\nimport type { DialogDefaults } from \"./defaults\";\nexport type { DialogDefaults } from \"./defaults\";\nimport { cn } from \"../../lib/utils\";\nimport overlay from \"../_shared/overlay.module.css\";\n\n/** A focused task over the page: create, invite, rename. open / defaultOpen / onOpenChange. */\nexport const Dialog = RadixDialog.Root;\nexport const DialogTrigger = RadixDialog.Trigger;\nexport const DialogClose = RadixDialog.Close;\n\nconst WIDTH = { sm: \"380px\", md: \"440px\", lg: \"560px\", xl: \"720px\" } as const;\n\n/**\n * The dialog's stepped tray: header, body and footer in one column whose height eases to each step's content when\n * `step` changes (the shared useHeightMorph), so the dialog grows or settles in place, centred, instead of jumping. It\n * mounts with the open dialog, so it measures the dialog it is in.\n */\nfunction DialogTray({ step, className, children }: { step: unknown; className?: string; children: ReactNode }) {\n  const tray = useRef<HTMLDivElement>(null);\n  useHeightMorph(tray, step);\n  return (\n    <div ref={tray} data-slot=\"dialog-tray\" className={cn(\"flex min-h-0 flex-col gap-4\", className)}>\n      {children}\n    </div>\n  );\n}\n\nexport interface DialogContentProps extends Omit<ComponentProps<typeof RadixDialog.Content>, \"title\"> {\n  /** The task, as a heading; also the dialog's accessible name. */\n  title: ReactNode;\n  /** One or two lines under the title. */\n  description?: ReactNode;\n  /** Width preset: sm 380px, md 440px, lg 560px or xl 720px; defaults to md. */\n  width?: keyof typeof WIDTH;\n  /** Legacy width preset; omitted by default. @deprecated Use width instead. */\n  size?: keyof typeof WIDTH;\n  /** The × in the corner. Default true. */\n  showClose?: boolean;\n  /** Close on Escape and on a click outside. Set false while work is running that must not be abandoned (and hide\n   *  the × with showClose={false}); Cancel and Done stay yours to offer. Default true. */\n  dismissible?: boolean;\n  /** Buttons at the bottom; DialogFooter lays them out the same everywhere. */\n  footer?: ReactNode;\n  /** The step of a flow the dialog shows (a string or a number). When it changes, the dialog eases to the new step's\n   *  height in place instead of jumping (a stepped tray). */\n  step?: string | number;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: Partial<\n    Record<\"root\" | \"scrim\" | \"tray\" | \"header\" | \"title\" | \"description\" | \"body\" | \"footer\" | \"close\", string>\n  >;\n}\n\n/**\n * The dialog surface: scrim, title, description, body and footer. Focus moves in (to the first field, or an autoFocus\n * one), is trapped while open and returns to the trigger on close. When its content changes height at once (the next\n * step), it eases to the new height in place.\n */\nexport function DialogContent({\n  title,\n  description,\n  width,\n  size: sizeProp,\n  showClose = true,\n  dismissible = true,\n  footer,\n  step,\n  className,\n  classNames,\n  children,\n  style,\n  onPointerDownOutside,\n  onEscapeKeyDown,\n  onOpenAutoFocus,\n  onCloseAutoFocus,\n  ref,\n  ...props\n}: DialogContentProps) {\n  const i18n = useMessages(\"dialog\", englishMessages);\n\n  const openFrom = useOpenFrom(ref);\n  const defaults = useComponentDefaults<DialogDefaults>(\"dialog\", [\"size\"]);\n  useDeprecatedProp(\"DialogContent\", \"size\", \"width\", sizeProp !== undefined);\n  const size = width ?? sizeProp ?? defaults.size ?? \"md\";\n  const container = usePortalContainer();\n  const focus = useReturnFocus({ onOpenAutoFocus, onCloseAutoFocus });\n  if (container === null) return null;\n  return (\n    <RadixDialog.Portal container={container}>\n      <RadixDialog.Overlay data-slot=\"dialog-scrim\" className={cn(overlay.scrim, classNames?.scrim)} />\n      <RadixDialog.Content\n        ref={openFrom}\n        data-slot=\"dialog\"\n        className={cn(overlay.dialog, \"flex flex-col p-[var(--pui-pad-panel)]\", classNames?.root, className)}\n        style={{ \"--_dialog-width\": WIDTH[size], ...style } as CSSProperties}\n        onPointerDownOutside={(e) => {\n          if (!dismissible) e.preventDefault();\n          onPointerDownOutside?.(e);\n        }}\n        onEscapeKeyDown={(e) => {\n          if (!dismissible) e.preventDefault();\n          onEscapeKeyDown?.(e);\n        }}\n        {...focus}\n        {...props}\n      >\n        <DialogTray step={step} className={classNames?.tray}>\n          <div data-slot=\"dialog-header\" className={cn(\"grid shrink-0 gap-1 pe-8\", classNames?.header)}>\n            <RadixDialog.Title\n              data-slot=\"dialog-title\"\n              className={cn(\"text-subheading leading-snug text-ink\", classNames?.title)}\n            >\n              {title}\n            </RadixDialog.Title>\n            {description ? (\n              <RadixDialog.Description\n                data-slot=\"dialog-description\"\n                className={cn(\"text-body-sm text-ink-2\", classNames?.description)}\n              >\n                {description}\n              </RadixDialog.Description>\n            ) : (\n              <RadixDialog.Description className=\"sr-only\">\n                {typeof title === \"string\" ? title : i18n(\"dialog\")}\n              </RadixDialog.Description>\n            )}\n          </div>\n          {/* the body scrolls alone so the header and the footer's actions stay on screen; its 4px bleed keeps a field's\n            focus halo from being cut off by the scroll box */}\n          <div data-slot=\"dialog-body\" className={cn(\"-m-1 grid min-h-0 gap-4 overflow-y-auto p-1\", classNames?.body)}>\n            {children}\n          </div>\n          {footer && <DialogFooter className={cn(\"shrink-0\", classNames?.footer)}>{footer}</DialogFooter>}\n        </DialogTray>\n        {showClose && (\n          <RadixDialog.Close asChild>\n            <Button\n              data-slot=\"dialog-close\"\n              variant=\"ghost\"\n              size=\"sm\"\n              aria-label={i18n(\"close\")}\n              className={cn(\"absolute end-3 top-3\", classNames?.close)}\n            >\n              <X className=\"size-4\" />\n            </Button>\n          </RadixDialog.Close>\n        )}\n      </RadixDialog.Content>\n    </RadixDialog.Portal>\n  );\n}\n\n/** The action row: secondary actions first, the main action last, aligned to the end. In a narrow dialog (a phone) the\n *  buttons share the row in equal parts, so each is a full-size target. */\nexport function DialogFooter({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      className={cn(\"mt-1 flex flex-wrap items-center justify-end gap-2 @max-xs:*:flex-1\", className)}\n      {...props}\n    />\n  );\n}\n\nexport default Dialog;\n"},{"path":"beamline/components/dialog/messages.ts","type":"registry:file","target":"src/ui/beamline/components/dialog/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  dialog: \"Dialog\",\n  close: \"Close\",\n} satisfies MessageDefinitions;\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/glow/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/glow/defaults.ts","content":"/** Pure allowed presentation defaults for Glow; no component or runtime imports. */\nexport interface GlowDefaults {\n  /** Shared effect setting; omit to inherit the component default. Scoped defaults retain the legacy key variant. */\n  variant?: \"aurora\" | \"ring\";\n}\n"},{"path":"beamline/components/glow/glow.module.css","type":"registry:file","target":"src/ui/beamline/components/glow/glow.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Light that says \"working\". Aurora: colour pools under and behind the element and drifts. Ring: a band of colour\n   travels around the edge. Both are blurred layers that only translate or rotate (compositor work), fade in and out\n   with `active`, stop entirely when inactive, and hold still off screen. Colours come from --pui-glow-1/2/3; every\n   period is a multiple of --pui-duration-considered, so the scope's animation speed reaches the light too. */\n  .root {\n    --pui-r: var(--pui-radius-card);\n    --pui-i: 1;\n    position: relative;\n    isolation: isolate;\n    border-radius: var(--pui-r);\n  }\n  .root > .field {\n    position: absolute;\n    z-index: -1;\n    pointer-events: none;\n    opacity: 0;\n    transition: opacity calc(var(--pui-duration-considered) * 2) var(--pui-ease-standard);\n  }\n  .root[data-active] > .field {\n    opacity: calc(0.9 * var(--pui-i));\n  }\n\n  /* aurora: three soft blobs along the lower edge, each drifting on its own period */\n  .aurora {\n    inset: 30% -6% -34%;\n    filter: blur(22px);\n  }\n  .blob {\n    position: absolute;\n    bottom: 0;\n    width: 46%;\n    height: 70%;\n    border-radius: 50%;\n    will-change: transform;\n  }\n  .blob:nth-child(1) {\n    left: 2%;\n    background: var(--pui-glow-1);\n    animation: glow-drift-a calc(var(--pui-duration-considered) * 19.5) var(--pui-ease-in-out) infinite alternate;\n  }\n  .blob:nth-child(2) {\n    left: 30%;\n    background: var(--pui-glow-2);\n    animation: glow-drift-b calc(var(--pui-duration-considered) * 25) var(--pui-ease-in-out) infinite alternate;\n  }\n  .blob:nth-child(3) {\n    right: 2%;\n    background: var(--pui-glow-3);\n    animation: glow-drift-c calc(var(--pui-duration-considered) * 22) var(--pui-ease-in-out) infinite alternate;\n  }\n\n  /* ring: a conic band (a square as wide as the element's diagonal, set from its size) rotating inside a mask that leaves only a thin frame plus its soft bloom */\n  .ring {\n    inset: -1px;\n    border-radius: calc(var(--pui-r) + 1px);\n    padding: 1.5px;\n    overflow: hidden;\n    -webkit-mask:\n      linear-gradient(#000 0 0) content-box exclude,\n      linear-gradient(#000 0 0);\n    mask:\n      linear-gradient(#000 0 0) content-box exclude,\n      linear-gradient(#000 0 0);\n  }\n  .bloom {\n    inset: -10px;\n    border-radius: calc(var(--pui-r) + 10px);\n    overflow: hidden;\n    filter: blur(12px);\n  }\n  .band {\n    position: absolute;\n    left: 50%;\n    top: 50%;\n    width: var(--pui-d, 150%);\n    aspect-ratio: 1;\n    translate: -50% -50%;\n    will-change: transform;\n    background: conic-gradient(\n      from 0deg,\n      transparent 0 58%,\n      var(--pui-glow-1) 70%,\n      var(--pui-glow-2) 80%,\n      var(--pui-glow-3) 88%,\n      transparent 96%\n    );\n    animation: glow-turn calc(var(--pui-duration-considered) * 10) linear infinite;\n  }\n  .bloom .band {\n    opacity: 0.55;\n  }\n\n  .root:not([data-active]) .blob,\n  .root:not([data-active]) .band {\n    animation-play-state: paused;\n  }\n  .root[data-paused] .blob,\n  .root[data-paused] .band {\n    animation-play-state: paused;\n  }\n  .root[data-reduced] .blob,\n  .root[data-reduced] .band {\n    animation: none;\n  }\n  @media (prefers-reduced-motion: reduce) {\n    .blob,\n    .band {\n      animation: none !important;\n    }\n  }\n}\n@keyframes glow-drift-a {\n  to {\n    transform: translate(18%, -12%) scale(1.15);\n  }\n}\n@keyframes glow-drift-b {\n  to {\n    transform: translate(-14%, 8%) scale(0.9);\n  }\n}\n@keyframes glow-drift-c {\n  to {\n    transform: translate(-20%, -10%) scale(1.1);\n  }\n}\n@keyframes glow-turn {\n  to {\n    transform: rotate(1turn);\n  }\n}\n"},{"path":"beamline/components/glow/glow.tsx","type":"registry:file","target":"src/ui/beamline/components/glow/glow.tsx","content":"\"use client\";\n\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { GlowDefaults } from \"./defaults\";\nexport type { GlowDefaults } from \"./defaults\";\n\nimport { useEffect, useRef, type CSSProperties, type ReactNode, type Ref } from \"react\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./glow.module.css\";\n\nexport interface GlowClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the field part; no override by default. */\n  field?: string;\n  /** Classes for the blob part; no override by default. */\n  blob?: string;\n  /** Classes for the bloom part; no override by default. */\n  bloom?: string;\n  /** Classes for the ring part; no override by default. */\n  ring?: string;\n  /** Classes for the band part; no override by default. */\n  band?: string;\n}\n\nexport interface GlowProps {\n  /** Content rendered inside the component. Defaults to no children. */\n  children?: ReactNode;\n  /** While true the glow fades in and moves; false fades it out and stops all motion. */\n  active?: boolean;\n  /** Aurora light behind the content or a ring around it; defaults to aurora. */\n  effect?: \"aurora\" | \"ring\";\n  /** Legacy effect setting; omitted by default. @deprecated Use effect instead. */\n  variant?: \"aurora\" | \"ring\";\n  /** 0–1, how strong the light is. */\n  intensity?: number;\n  /** Corner radius of the wrapped element, so the ring follows it. Defaults to the card radius. */\n  radius?: string;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: GlowClassNames;\n}\n\n/**\n * Ambient light around one element that says \"something is happening here\": the composer while an agent works, the\n * live card, the call in progress. One running per screen (bind `active` to a state that ends). Compositor-only motion; nothing runs while inactive or off screen.\n */\nexport function Glow({\n  children,\n  active = true,\n  effect: effectProp,\n  variant: variantProp,\n  intensity = 1,\n  radius,\n  className,\n  classNames,\n  style,\n  ref,\n}: GlowProps) {\n  const scopeDefaults = useComponentDefaults<GlowDefaults>(\"glow\", [\"variant\"]);\n  useDeprecatedProp(\n    \"glow\",\n    \"variant\",\n    \"effect\",\n    variantProp !== undefined || (effectProp === undefined && scopeDefaults.variant !== undefined),\n  );\n  const variant = effectProp ?? variantProp ?? scopeDefaults.variant ?? \"aurora\";\n\n  const own = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(own, ref);\n  const tokens = useMotionTokens(own);\n  const managedReduced = useReducedMotionSafe();\n  const onScreen = useActive(own);\n  // The ring's rotating band must cover the element's diagonal and no more (a larger layer only costs GPU memory).\n  useEffect(() => {\n    const el = own.current;\n    if (!el || variant !== \"ring\" || typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => {\n      const { width, height } = e.contentRect;\n      el.style.setProperty(\"--pui-d\", `${Math.ceil(Math.hypot(width, height) + 24)}px`);\n    });\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [variant]);\n  return (\n    <div\n      ref={attach}\n      data-slot=\"glow\"\n      data-variant={variant}\n      data-active={active || undefined}\n      data-paused={onScreen ? undefined : \"\"}\n      data-reduced={tokens.reduced || managedReduced ? \"\" : undefined}\n      className={cn(styles.root, classNames?.root, className)}\n      style={{ ...(radius ? { \"--pui-r\": radius } : {}), \"--pui-i\": intensity, ...style } as CSSProperties}\n    >\n      {variant === \"aurora\" ? (\n        <span data-slot=\"glow-field\" aria-hidden=\"true\" className={cn(styles.field, styles.aurora, classNames?.field)}>\n          <span data-slot=\"glow-blob\" className={cn(styles.blob, classNames?.blob)} />\n          <span data-slot=\"glow-blob\" className={cn(styles.blob, classNames?.blob)} />\n          <span data-slot=\"glow-blob\" className={cn(styles.blob, classNames?.blob)} />\n        </span>\n      ) : (\n        <>\n          <span\n            data-slot=\"glow-bloom\"\n            aria-hidden=\"true\"\n            className={cn(styles.field, styles.bloom, classNames?.field, classNames?.bloom)}\n          >\n            <span data-slot=\"glow-band\" className={cn(styles.band, classNames?.band)} />\n          </span>\n          <span\n            data-slot=\"glow-ring\"\n            aria-hidden=\"true\"\n            className={cn(styles.field, styles.ring, classNames?.field, classNames?.ring)}\n          >\n            <span data-slot=\"glow-band\" className={cn(styles.band, classNames?.band)} />\n          </span>\n        </>\n      )}\n      {children}\n    </div>\n  );\n}\n\nexport default Glow;\n"},{"path":"beamline/components/hold-to-confirm/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/hold-to-confirm/defaults.ts","content":"/** Pure allowed presentation defaults for HoldToConfirm; no component or runtime imports. */\nexport interface HoldToConfirmDefaults {\n  /** Shared tone setting; omit to inherit the surrounding scope or the component default. */\n  tone?: \"danger\" | \"neutral\";\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\" | \"touch\";\n}\n"},{"path":"beamline/components/hold-to-confirm/hold-to-confirm.module.css","type":"registry:file","target":"src/ui/beamline/components/hold-to-confirm/hold-to-confirm.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Held, the control sinks a little at once (a wide control presses shallower than a small one), so the hold is\n     acknowledged before the fill has visibly moved; reduced motion keeps only the fill. */\n  [data-slot=\"hold-to-confirm\"].root {\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      scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root[data-state=\"holding\"] {\n    scale: calc(1 - (1 - var(--pui-press-scale, 0.955)) / 3);\n    transition-duration: 0ms;\n  }\n  :global(.pui[data-motion=\"reduced\"]) .root[data-state=\"holding\"],\n  :global(.pui[data-motion=\"none\"]) .root[data-state=\"holding\"] {\n    scale: none;\n  }\n  @media (prefers-reduced-motion: reduce) {\n    .root[data-state=\"holding\"] {\n      scale: none;\n    }\n  }\n  /* the words keep their path when the hold lands: the width eases from the old to the new */\n  .words {\n    display: inline-flex;\n    overflow: clip;\n    white-space: nowrap;\n  }\n  /* a press let go too soon: the fill runs a little way and drains, showing what holding does */\n  .root[data-hint] > .fill {\n    animation: hint calc(var(--_hold) * 0.6) var(--pui-ease-default);\n  }\n  @keyframes hint {\n    40% {\n      transform: scaleX(0.22);\n    }\n  }\n  /* The hold fill: the control's own text colour at a quarter strength (red for danger, ink for neutral), sweeping\n     from the inline start for exactly the hold time, linearly, so its edge is a true progress mark. It sits over the\n     hover tint, so held and not-yet-held parts stay distinct. Let go early and it drains back on the standard ease;\n     once confirmed it stays full. The sweep is information, so reduced motion keeps it; only the drain becomes instant. */\n  .fill {\n    position: absolute;\n    inset: 0;\n    z-index: -1;\n    background: color-mix(in oklab, currentColor 24%, transparent);\n    transform: scaleX(0);\n    transform-origin: left;\n    transition: transform var(--pui-duration-standard) var(--pui-ease-standard);\n    pointer-events: none;\n  }\n  .fill:dir(rtl) {\n    transform-origin: right;\n  }\n  :where([data-state=\"holding\"]) > .fill {\n    transform: none;\n    transition: transform var(--_hold) linear;\n  }\n  :where([data-state=\"confirmed\"]) > .fill {\n    transform: none;\n  }\n  :where([data-tone=\"neutral\"]) > .fill {\n    background: color-mix(in oklab, currentColor 14%, transparent);\n  }\n}\n"},{"path":"beamline/components/hold-to-confirm/hold-to-confirm.tsx","type":"registry:file","target":"src/ui/beamline/components/hold-to-confirm/hold-to-confirm.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { type Ref } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { HoldToConfirmDefaults } from \"./defaults\";\nexport type { HoldToConfirmDefaults } from \"./defaults\";\n\nimport { useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { buttonClassName, type ButtonSize } from \"../button/button\";\nimport { useWidthMorph } from \"../../hooks/use-width-morph\";\nimport { cn } from \"../../lib/utils\";\nimport { usePressHold } from \"../../hooks/use-press-hold\";\nimport { useExpiring } from \"../../hooks/use-expiring\";\nimport styles from \"./hold-to-confirm.module.css\";\n\nexport interface HoldToConfirmClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the fill part; no override by default. */\n  fill?: string;\n  /** Classes for the content part; no override by default. */\n  content?: string;\n  /** Classes for the hint part; no override by default. */\n  hint?: string;\n}\n\nexport interface HoldToConfirmProps {\n  /** Say the hold: \"Hold to delete workspace\". */\n  label: string;\n  /** Shown once confirmed. Default \"Done\". */\n  confirmedLabel?: string;\n  /** Runs the confirmation action; required handler. */\n  onConfirm: () => void;\n  /** How long to hold, ms. Default 1200. */\n  duration?: number;\n  /** Icon element shown beside the label. Defaults to no leading icon. */\n  icon?: ReactNode;\n  /** Semantic colour of the displayed content. Defaults to \"danger\". */\n  tone?: \"danger\" | \"neutral\";\n  /** Confirmed state, controlled; uncontrolled it stays confirmed after the hold. */\n  confirmed?: boolean;\n  /** Reports hold state after a change; no callback by default. */\n  onHoldChange?: (holding: boolean) => void;\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: ButtonSize;\n  /** Prevents interaction with the control. Defaults to false. */\n  disabled?: boolean;\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?: HoldToConfirmClassNames;\n  /** Receives the button or trigger; no ref by default. */\n  ref?: Ref<HTMLButtonElement>;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n}\n\n/**\n * Confirms a costly action by holding instead of clicking: a fill sweeps across while held and drains if let go\n * early. Pointer, touch, Space and Enter all hold. Use it where a dialog would interrupt but a click is too cheap.\n */\nexport function HoldToConfirm({\n  label,\n  confirmedLabel: confirmedLabelProp,\n  onConfirm,\n  duration = 1200,\n  icon,\n  tone: toneProp,\n  confirmed: confirmedProp,\n  onHoldChange,\n  size: sizeProp,\n  disabled,\n  className,\n  classNames,\n  ref,\n  style,\n}: HoldToConfirmProps) {\n  const i18n = useMessages(\"hold-to-confirm\", englishMessages);\n  const confirmedLabel = confirmedLabelProp === undefined ? i18n(\"done\") : confirmedLabelProp;\n\n  const scopeDefaults = useComponentDefaults<HoldToConfirmDefaults>(\"hold-to-confirm\", [\"tone\", \"size\"]);\n  const tone = toneProp ?? scopeDefaults.tone ?? \"danger\";\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const [inner, setInner] = useState(false);\n  const confirmed = confirmedProp ?? inner;\n  // a press let go too soon to mean anything shows once what holding would do (the fill runs a little way and back)\n  const [hint, setHint] = useExpiring<true>(1000);\n  const startedAt = useRef(0);\n  const timer = useRef<ReturnType<typeof setTimeout>>(undefined);\n  // the words change size in place when the hold lands (\"Hold to delete workspace\" → \"Deletion scheduled\")\n  const words = useRef<HTMLSpanElement>(null);\n  useWidthMorph(words, confirmed);\n  const hintId = useId();\n  const host = useRef<HTMLButtonElement>(null);\n  const attach = useComposedRef(host, ref);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = tokens.reduced || managedReduced;\n  useEffect(() => () => clearTimeout(timer.current), []);\n  useEffect(() => {\n    if (confirmedProp === false) setInner(false);\n  }, [confirmedProp]);\n\n  // the shared press and hold: pointer, finger, Space and Enter hold; Escape, a lost pointer or focus let go before it\n  // commits\n  const hold = usePressHold({\n    disabled: disabled || confirmed,\n    onStart: () => {\n      setHint(null);\n      startedAt.current = performance.now();\n      onHoldChange?.(true);\n      timer.current = setTimeout(() => {\n        hold.end(\"release\");\n        setInner(true);\n        navigator.vibrate?.(12);\n        onConfirm();\n      }, duration);\n    },\n    onEnd: (how) => {\n      clearTimeout(timer.current);\n      onHoldChange?.(false);\n      if (how === \"release\" && performance.now() - startedAt.current < duration * 0.25) setHint(true, duration * 0.6);\n    },\n  });\n  const pressed = hold.pressing;\n\n  return (\n    <button\n      type=\"button\"\n      ref={attach}\n      style={style}\n      data-slot=\"hold-to-confirm\"\n      data-tone={tone}\n      data-state={confirmed ? \"confirmed\" : pressed ? \"holding\" : \"idle\"}\n      data-hint={hint && !confirmed && !pressed ? \"\" : undefined}\n      disabled={disabled}\n      aria-describedby={confirmed ? undefined : hintId}\n      className={cn(\n        buttonClassName({ variant: tone === \"danger\" ? \"danger\" : \"secondary\", size }),\n        styles.root,\n        \"touch-none overflow-hidden\",\n        confirmed && \"cursor-default\",\n        classNames?.root,\n        className,\n      )}\n      {...hold.handlers}\n    >\n      <span\n        aria-hidden\n        data-slot=\"hold-to-confirm-fill\"\n        className={cn(styles.fill, classNames?.fill)}\n        style={{ \"--_hold\": `${duration}ms` } as CSSProperties}\n      />\n      <span ref={words} className={styles.words}>\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={confirmed ? \"done\" : \"hold\"}\n            data-slot=\"hold-to-confirm-content\"\n            className={cn(\"inline-flex items-center gap-1.5\", classNames?.content)}\n            initial={{ opacity: 0, y: reduced ? 0 : 6 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: reduced ? 0 : -6 }}\n            transition={{ duration: reduced ? 0 : tokens.duration.out }}\n          >\n            {confirmed ? <Check /> : icon}\n            {confirmed ? confirmedLabel : label}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n      <span id={hintId} data-slot=\"hold-to-confirm-hint\" className={cn(\"sr-only\", classNames?.hint)}>\n        {i18n(\"holdHint\", { count: Math.round(duration / 100) / 10 })}\n      </span>\n    </button>\n  );\n}\n\nexport default HoldToConfirm;\n"},{"path":"beamline/components/hold-to-confirm/messages.ts","type":"registry:file","target":"src/ui/beamline/components/hold-to-confirm/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  done: \"Done\",\n  holdHint: {\n    one: \"Press and hold for {count} second to confirm.\",\n    other: \"Press and hold for {count} seconds to confirm.\",\n  },\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/input/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/input/defaults.ts","content":"export interface InputDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/input/input.tsx","type":"registry:file","target":"src/ui/beamline/components/input/input.tsx","content":"\"use client\";\n\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { useId, useMemo, useRef, useState, type InputHTMLAttributes, type ReactNode, type Ref } from \"react\";\nimport * as RadixPopover from \"@radix-ui/react-popover\";\nimport { Listbox } from \"../_shared/listbox\";\nimport overlay from \"../_shared/overlay.module.css\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\nimport {\n  Field,\n  FieldCount,\n  FieldFrame,\n  fieldControlClass,\n  useFieldIds,\n  type FieldSize,\n  type FieldClassNames,\n} from \"../_shared/field\";\n\nimport { cn } from \"../../lib/utils\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { InputDefaults } from \"./defaults\";\nexport type { InputDefaults } from \"./defaults\";\n\nexport interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"size\"> {\n  /** Visible label and accessible name. */\n  label: ReactNode;\n  /** Reports the text after an edit; no callback by default, and native onChange still receives its event. */\n  onValueChange?: (value: string) => void;\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 field. */\n  description?: ReactNode;\n  /** The problem and its fix; marks the field invalid. */\n  error?: ReactNode;\n  /** The confirmation that the value works (\"@mira is available\"): replaces the description, and the ring at the end\n   *  closes into a check. */\n  success?: ReactNode;\n  /** A check or save of the value is running: a spinner at the end, the field stays editable (aria-busy). */\n  loading?: boolean;\n  /** With `maxLength`: the count in the label's row, quiet until the field has attention, in words near the limit. */\n  showCount?: boolean;\n  /** Height 32 / 36 / 40 px. */\n  size?: FieldSize;\n  /** Content before the text: an icon, a unit, a prefix like \"https://\". */\n  leading?: ReactNode;\n  /** Content after the text: a unit, a status, an icon button. */\n  trailing?: ReactNode;\n  /** Whole values to offer for what is typed, shown under the field: ↑ ↓ move, Enter (or Tab after moving) takes one,\n   *  Escape keeps what was typed. What the person typed is never replaced unless they take a suggestion. */\n  complete?: (text: string) => string[];\n  /** Email entry: offers the typed name with each of these domains (\"mira@\" → mira@gmail.com …), narrowed by what is\n   *  typed after the @. A shorthand for `complete`. */\n  emailDomains?: string[];\n  /** Lands on the outer field (label, frame, message). */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: FieldClassNames & { input?: string };\n  /** Receives the native input; no ref by default. */\n  ref?: Ref<HTMLInputElement>;\n}\n\n/** A single-line text field with its label, help and error wired for assistive tech. */\nexport function Input({\n  label,\n  hideLabel: hideLabelProp,\n  showLabel,\n  description,\n  error,\n  success,\n  loading,\n  showCount,\n  size: sizeProp,\n  leading,\n  trailing,\n  id,\n  className,\n  classNames,\n  style,\n  required,\n  disabled,\n  readOnly,\n  maxLength,\n  value,\n  defaultValue,\n  onChange,\n  onValueChange,\n  complete,\n  emailDomains,\n  ref,\n  ...props\n}: InputProps) {\n  useDeprecatedProp(\"Input\", \"hideLabel\", \"showLabel={false}\", hideLabelProp !== undefined);\n  const hideLabel = showLabel === undefined ? hideLabelProp : !showLabel;\n  const defaults = useComponentDefaults<InputDefaults>(\"input\", [\"size\"]);\n  const size = sizeProp ?? defaults.size ?? \"md\";\n  const { controlId, descriptionId, errorId, aria } = useFieldIds(id, { description, error, success });\n  // the count follows the text, typed or set by the caller\n  const [typed, setTyped] = useState(String(defaultValue ?? \"\").length);\n  const length = value !== undefined ? String(value).length : typed;\n  const counted = !!showCount && maxLength != null;\n  const completes = !!complete || !!emailDomains?.length;\n  const [text, setText] = useState(String(defaultValue ?? \"\"));\n  const current = value !== undefined ? String(value) : text;\n  const [focused, setFocused] = useState(false);\n  const [dismissed, setDismissed] = useState<string | null>(null);\n  const [active, setActive] = useState(-1);\n  const viaKeys = useRef(false);\n  const input = useRef<HTMLInputElement>(null);\n  const attach = useComposedRef(input, ref);\n  const listId = useId();\n  const container = usePortalContainer();\n  const offers = useMemo(() => {\n    if (!completes || !current) return [];\n    const list = complete ? complete(current) : emailOffers(current, emailDomains ?? []);\n    // an offer the field already holds whole is no offer\n    return list.filter((s) => s !== current).slice(0, 8);\n  }, [completes, complete, emailDomains, current]);\n  const open = completes && focused && offers.length > 0 && dismissed !== current && !disabled && !readOnly;\n  const take = (s: string) => {\n    const el = input.current;\n    if (!el) return;\n    // through the native setter, so onChange and onValueChange hear it like typing, controlled or not\n    Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, \"value\")?.set?.call(el, s);\n    el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    setDismissed(s);\n    setActive(-1);\n  };\n  const field = (\n    <input\n      ref={attach}\n      data-slot=\"input\"\n      {...aria}\n      aria-busy={loading || undefined}\n      required={required}\n      disabled={disabled}\n      readOnly={readOnly}\n      maxLength={maxLength}\n      value={value}\n      defaultValue={defaultValue}\n      onChange={(e) => {\n        if (counted) setTyped(e.target.value.length);\n        if (completes) {\n          setText(e.target.value);\n          setActive(-1);\n        }\n        onChange?.(e);\n        onValueChange?.(e.target.value);\n      }}\n      className={cn(fieldControlClass, classNames?.input)}\n      {...(completes\n        ? {\n            role: \"combobox\",\n            autoComplete: \"off\",\n            \"aria-autocomplete\": \"list\" as const,\n            \"aria-expanded\": open,\n            \"aria-controls\": open ? listId : undefined,\n            \"aria-activedescendant\": open && active >= 0 ? `${listId}-${active}` : undefined,\n          }\n        : {})}\n      {...props}\n      onFocus={(e) => {\n        if (completes) setFocused(true);\n        props.onFocus?.(e);\n      }}\n      onBlur={(e) => {\n        if (completes) {\n          setFocused(false);\n          setActive(-1);\n        }\n        props.onBlur?.(e);\n      }}\n      onKeyDown={(e) => {\n        if (open) {\n          if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n            e.preventDefault();\n            viaKeys.current = true;\n            const n = offers.length;\n            setActive((a) => (e.key === \"ArrowDown\" ? (a + 1) % n : a <= 0 ? n - 1 : a - 1));\n          } else if ((e.key === \"Enter\" || e.key === \"Tab\") && active >= 0) {\n            if (e.key === \"Enter\") e.preventDefault();\n            take(offers[active]);\n          } else if (e.key === \"Escape\") {\n            e.preventDefault();\n            e.stopPropagation();\n            setDismissed(current);\n          }\n        }\n        props.onKeyDown?.(e);\n      }}\n    />\n  );\n  const frame = (\n    <FieldFrame\n      size={size}\n      classNames={classNames}\n      invalid={!!error}\n      disabled={disabled}\n      readOnly={readOnly}\n      status={loading ? \"busy\" : success && !error ? \"success\" : undefined}\n      leading={leading}\n      trailing={trailing}\n    >\n      {completes ? (\n        field\n      ) : (\n        <input\n          ref={ref}\n          data-slot=\"input\"\n          {...aria}\n          aria-busy={loading || undefined}\n          required={required}\n          disabled={disabled}\n          readOnly={readOnly}\n          maxLength={maxLength}\n          value={value}\n          defaultValue={defaultValue}\n          onChange={(e) => {\n            if (counted) setTyped(e.target.value.length);\n            onChange?.(e);\n            onValueChange?.(e.target.value);\n          }}\n          className={cn(fieldControlClass, classNames?.input)}\n          {...props}\n        />\n      )}\n    </FieldFrame>\n  );\n  return (\n    <Field\n      label={label}\n      hideLabel={hideLabel}\n      description={description}\n      error={error}\n      success={error ? undefined : success}\n      aside={showCount && maxLength != null ? <FieldCount length={length} max={maxLength} /> : undefined}\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      {completes ? (\n        <RadixPopover.Root open={open}>\n          <RadixPopover.Anchor asChild>\n            <div>{frame}</div>\n          </RadixPopover.Anchor>\n          {container !== null && (\n            <RadixPopover.Portal container={container}>\n              <RadixPopover.Content\n                data-slot=\"input-suggestions\"\n                role=\"presentation\"\n                sideOffset={6}\n                align=\"start\"\n                collisionPadding={8}\n                onOpenAutoFocus={(e) => e.preventDefault()}\n                onCloseAutoFocus={(e) => e.preventDefault()}\n                className={cn(overlay.surface, \"w-[var(--radix-popper-anchor-width)] min-w-[min(240px,100vw)]\")}\n              >\n                <Listbox\n                  id={listId}\n                  label={typeof label === \"string\" ? label : String(props.name ?? \"\")}\n                  options={offers.map((o) => ({ value: o, label: o }))}\n                  query={current}\n                  active={active}\n                  onActive={setActive}\n                  viaKeys={viaKeys}\n                  isSelected={() => false}\n                  onPick={(o) => take(o.value)}\n                />\n              </RadixPopover.Content>\n            </RadixPopover.Portal>\n          )}\n        </RadixPopover.Root>\n      ) : (\n        frame\n      )}\n    </Field>\n  );\n}\n\n/** The typed name with each domain, narrowed by what follows the @; nothing before a name is typed. */\nexport function emailOffers(text: string, domains: string[]) {\n  const at = text.indexOf(\"@\");\n  const name = (at < 0 ? text : text.slice(0, at)).trim();\n  if (!name || /\\s/.test(name)) return [];\n  const typed = at < 0 ? \"\" : text.slice(at + 1).toLowerCase();\n  return domains.filter((d) => d.toLowerCase().startsWith(typed)).map((d) => `${name}@${d}`);\n}\n\nexport default Input;\n"},{"path":"beamline/components/item/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/item/defaults.ts","content":"/** Pure allowed presentation defaults for Item; no component or runtime imports. */\nexport interface ItemDefaults {\n  /** Shared visual weight; ghost by default. @deprecated Use ghost for plain, or secondary for card. */\n  variant?: \"ghost\" | \"outline\" | \"secondary\" | \"plain\" | \"card\";\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\n"},{"path":"beamline/components/item/item.module.css","type":"registry:file","target":"src/ui/beamline/components/item/item.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Item: one row. Plain rows are bare until pointed at; outline draws a quiet hairline; card wears the surface role.\n     Selected (O1 §6): accent tint, a 2px accent-ink bar on the inline-start side, and every line of text in ink, so the\n     state never rests on colour alone and the text keeps 4.5:1 on the tint in every look. */\n  .root {\n    position: relative;\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-2);\n    min-width: 0;\n    padding-inline: var(--pui-space-3);\n    border-radius: var(--pui-radius-control);\n    /* the corner an icon tile in the row's corner zone must take (O1 nested radius = outer − inset) */\n    --_item-nest: calc(var(--pui-radius-control) - var(--_pb, var(--pui-space-2-5)));\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      var(--m-transition);\n  }\n  .outline {\n    --_item-nest: calc(var(--pui-radius-card) - var(--_pb, var(--pui-space-2-5)));\n    border-radius: var(--pui-radius-card);\n    box-shadow: var(--pui-shadow-hairline);\n  }\n  .card {\n    --_item-nest: calc(var(--pui-radius-card) - var(--_pb, var(--pui-space-2-5)));\n    border-radius: var(--pui-radius-card);\n  }\n  .root[data-interactive]:hover {\n    background-color: var(--pui-hover);\n    transition-duration: 0ms;\n  }\n  /* a press steps one more rung at once */\n  .root[data-interactive]:has(> .main:active) {\n    background-color: var(--pui-hover-2);\n    transition-duration: 0ms;\n  }\n  .root[data-state=\"selected\"],\n  .root[data-state=\"selected\"]:hover {\n    background-color: var(--pui-accent-tint);\n  }\n  /* the selection bar is always there and grows from its middle when the row is chosen (a transition, so a row that\n     starts selected simply is) */\n  .root::before {\n    content: \"\";\n    position: absolute;\n    inset-block: 8px;\n    inset-inline-start: 0;\n    width: 2px;\n    border-radius: var(--pui-radius-pill);\n    background: var(--pui-accent-ink);\n    opacity: 0;\n    scale: 1 0.3;\n    transition:\n      opacity var(--pui-motion-out) var(--pui-ease-default),\n      scale var(--pui-motion-out) var(--pui-ease-default);\n    pointer-events: none;\n  }\n  .root[data-state=\"selected\"]::before {\n    opacity: 1;\n    scale: 1 1;\n    transition-duration: var(--pui-duration-fast);\n  }\n  .main {\n    display: flex;\n    flex: 1;\n    min-width: 0;\n    align-items: center;\n    gap: var(--pui-space-3);\n    padding-block: var(--pui-space-2-5);\n    border-radius: var(--pui-radius-control);\n    color: inherit;\n    text-align: start;\n    text-decoration: none;\n    outline-offset: 2px;\n  }\n  .sm {\n    --_pb: var(--pui-space-1-5);\n  }\n  .sm .main {\n    padding-block: var(--pui-space-1-5);\n  }\n  .media {\n    display: grid;\n    flex: none;\n    place-items: center;\n  }\n  .text {\n    display: flex;\n    flex: 1;\n    flex-direction: column;\n    min-width: 0;\n  }\n  .title {\n    overflow: hidden;\n    color: var(--pui-ink);\n    font-size: var(--pui-text-base);\n    font-weight: var(--pui-weight-medium);\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .sm .title {\n    font-size: var(--pui-text-sm);\n  }\n  .description {\n    overflow: hidden;\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .meta {\n    flex: none;\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    font-variant-numeric: tabular-nums;\n    unicode-bidi: isolate;\n  }\n  .root[data-state=\"selected\"] :is(.description, .meta) {\n    color: var(--pui-ink);\n  }\n  .actions {\n    display: flex;\n    flex: none;\n    align-items: center;\n    gap: var(--pui-space-1);\n  }\n  /* a narrow room (below 20rem, measured) joins what a wide one separates: the meta moves onto the description line,\n     so the title keeps the whole width (a row with no description keeps its meta beside the title) */\n  .root[data-narrow] .main:has(.description) {\n    display: grid;\n    grid-template-columns: auto minmax(0, 1fr) auto;\n    grid-template-areas:\n      \"media title title\"\n      \"media description meta\";\n    column-gap: var(--pui-space-3);\n    align-items: center;\n  }\n  .root[data-narrow] .main:has(.description) .text {\n    display: contents;\n  }\n  .root[data-narrow] .main:has(.description) .media {\n    grid-area: media;\n  }\n  .root[data-narrow] .main:has(.description) .title {\n    grid-area: title;\n  }\n  .root[data-narrow] .main:has(.description) .description {\n    grid-area: description;\n  }\n  .root[data-narrow] .main:has(.description) .meta {\n    grid-area: meta;\n    align-self: baseline;\n    font-size: var(--pui-text-2xs);\n  }\n  /* ItemIcon: a tinted tile behind an icon; its corner follows the look's control radius. */\n  .icon {\n    display: grid;\n    width: 32px;\n    height: 32px;\n    place-items: center;\n    /* its own control corner, or concentric with the row (never squarer than a chip, O1 §14.1) when the row's corner\n       is round enough to reach it; the 100000× term switches between the two without a conditional */\n    --_r: max(\n      var(--_item-nest, -1px),\n      var(--pui-radius-chip),\n      var(--pui-radius-control) - max(var(--_item-nest, -1px), 0px) * 100000\n    );\n    border-radius: var(--_r);\n    background: var(--_tile, var(--pui-hover-2));\n    color: var(--_glyph, var(--pui-ink-2));\n  }\n  .icon > svg {\n    width: 16px;\n    height: 16px;\n  }\n  .icon[data-tone=\"accent\"] {\n    --_tile: var(--pui-accent-tint);\n    --_glyph: var(--pui-accent-ink);\n  }\n  .icon[data-tone=\"green\"] {\n    --_tile: var(--pui-green-tint);\n    --_glyph: var(--pui-green);\n  }\n  .icon[data-tone=\"orange\"] {\n    --_tile: var(--pui-orange-tint);\n    --_glyph: var(--pui-orange);\n  }\n  .icon[data-tone=\"red\"] {\n    --_tile: var(--pui-red-tint);\n    --_glyph: var(--pui-red);\n  }\n  .icon[data-tone=\"blue\"] {\n    --_tile: var(--pui-blue-tint);\n    --_glyph: var(--pui-blue);\n  }\n  .icon[data-tone=\"violet\"] {\n    --_tile: var(--pui-violet-tint);\n    --_glyph: var(--pui-violet);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.card {\n  composes: m-surface from global;\n}\n"},{"path":"beamline/components/item/item.tsx","type":"registry:file","target":"src/ui/beamline/components/item/item.tsx","content":"\"use client\";\n\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { useEffect, useRef, type CSSProperties, type ComponentProps, type RefObject } from \"react\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { ItemDefaults } from \"./defaults\";\nexport type { ItemDefaults } from \"./defaults\";\n\nimport type { ReactNode, Ref } from \"react\";\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 styles from \"./item.module.css\";\n\nexport interface ItemClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the main part; no override by default. */\n  main?: string;\n  /** Classes for the media part; no override by default. */\n  media?: string;\n  /** Classes for the text part; no override by default. */\n  text?: 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 meta part; no override by default. */\n  meta?: string;\n  /** Classes for the actions part; no override by default. */\n  actions?: string;\n}\n\nexport interface ItemProps {\n  /** Heading shown above the content. Required. */\n  title: ReactNode;\n  /** Supporting text beneath the label or heading. Defaults to no supporting text. */\n  description?: ReactNode;\n  /** Icon tile, avatar or thumbnail on the start side. */\n  media?: ReactNode;\n  /** Right-aligned meta: time, count, status. */\n  meta?: ReactNode;\n  /** Trailing controls. Clicks inside do not trigger the row. They wait for attention (hover, focus in the row, the\n   *  row selected; always shown on touch) and stay while one of them is pressed or has its menu open. */\n  actions?: ReactNode;\n  /** Show the actions at rest instead of with attention. Default false. */\n  actionsAtRest?: boolean;\n  /** URL opened when the item is activated. Defaults to a non-link item. */\n  href?: string;\n  /** Called when an item is activated; no handler by default. */\n  onSelect?: () => void;\n  /** Controlled selected indication; omitted by default to use defaultSelected. */\n  selected?: boolean;\n  /** Initial selected indication when uncontrolled; defaults to false. */\n  defaultSelected?: boolean;\n  /** Reports selecting the row; no callback by default, and onSelect remains the activation event. */\n  onSelectedChange?: (selected: boolean) => void;\n  /** Visual weight; defaults to ghost. @deprecated The values plain and card alias ghost and secondary. */\n  variant?: \"ghost\" | \"outline\" | \"secondary\" | \"plain\" | \"card\";\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: \"sm\" | \"md\";\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?: ItemClassNames;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n}\n\n/** Watched by nothing: a row that has no fold to make measures nothing. */\nconst UNWATCHED: RefObject<HTMLDivElement | null> = { current: null };\n\nconst VARIANT: Record<NonNullable<ItemProps[\"variant\"]>, string | undefined> = {\n  ghost: undefined,\n  plain: undefined,\n  outline: styles.outline,\n  secondary: styles.card,\n  card: styles.card,\n};\n\n/**\n * The generic list row every list is built from: media, title, description, meta and actions. It becomes a link or a\n * button when given `href` / `onSelect`, keeping trailing actions separately focusable. It answers its room: below\n * 20rem the meta joins the description line so the title keeps the whole width; trailing actions wait for attention.\n */\nexport function Item({\n  title,\n  description,\n  media,\n  meta,\n  actions,\n  actionsAtRest = false,\n  href,\n  onSelect,\n  selected: selectedProp,\n  defaultSelected,\n  onSelectedChange,\n  variant: variantProp,\n  size: sizeProp,\n  className,\n  style,\n  classNames,\n  ref,\n}: ItemProps) {\n  const scopeDefaults = useComponentDefaults<ItemDefaults>(\"item\", [\"variant\", \"size\"]);\n  const requestedVariant = variantProp ?? scopeDefaults.variant ?? \"ghost\";\n  useDeprecatedProp(\"Item\", 'variant=\"plain\"', 'variant=\"ghost\"', requestedVariant === \"plain\");\n  useDeprecatedProp(\"Item\", 'variant=\"card\"', 'variant=\"secondary\"', requestedVariant === \"card\");\n  const variant = requestedVariant === \"ghost\" ? \"plain\" : requestedVariant === \"secondary\" ? \"card\" : requestedVariant;\n  const [selected, setSelected] = useControllable(selectedProp, defaultSelected ?? false);\n  const selectable = defaultSelected !== undefined || onSelectedChange !== undefined;\n  const choose = () => {\n    // an item nobody listens to stays a plain row: it keeps no selection of its own\n    if (selectable) setSelected(true);\n    onSelectedChange?.(true);\n    onSelect?.();\n  };\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const interactive = !!(href || onSelect || selectable);\n  // only a row with both a description and a meta has a fold to make, so only such a row measures its width (one\n  // observer, read when the width crosses 20rem); no containment, so a row can sit in a scrolling strip at its own width\n  const root = useRef<HTMLDivElement>(null);\n  const rootRef = useComposedRef(root, ref);\n  const narrow = useWidthAs(description && meta ? root : UNWATCHED, (w) => w < 320, false);\n  const tools = useRef<HTMLSpanElement>(null);\n  // the actions stay shown while one is pressed (a favourite that is on) or has its menu open; watched only for those\n  // attributes, so a still row costs nothing\n  const hasActions = !!actions && !actionsAtRest;\n  useEffect(() => {\n    const el = tools.current;\n    if (!el || !hasActions || typeof MutationObserver === \"undefined\") return;\n    const held = '[aria-pressed=\"true\"],[aria-expanded=\"true\"],[data-state=\"open\"]';\n    const update = () => el.toggleAttribute(\"data-shown\", !!el.querySelector(held));\n    update();\n    const watch = new MutationObserver(update);\n    watch.observe(el, {\n      subtree: true,\n      attributes: true,\n      attributeFilter: [\"aria-pressed\", \"aria-expanded\", \"data-state\"],\n    });\n    return () => watch.disconnect();\n  }, [hasActions]);\n  const body = (\n    <>\n      {media && (\n        <span data-slot=\"item-media\" className={cn(styles.media, classNames?.media)}>\n          {media}\n        </span>\n      )}\n      <span className={cn(styles.text, classNames?.text)} data-slot=\"item-text\">\n        <span data-slot=\"item-title\" className={cn(styles.title, classNames?.title)}>\n          {title}\n        </span>\n        {description && (\n          <span data-slot=\"item-description\" className={cn(styles.description, classNames?.description)}>\n            {description}\n          </span>\n        )}\n      </span>\n      {meta && (\n        <span data-slot=\"item-meta\" className={cn(styles.meta, classNames?.meta)}>\n          {meta}\n        </span>\n      )}\n    </>\n  );\n  return (\n    <div\n      ref={rootRef}\n      style={style}\n      data-slot=\"item\"\n      data-narrow={narrow || undefined}\n      data-variant={variant}\n      data-state={selected ? \"selected\" : undefined}\n      data-selected={selected || undefined}\n      data-interactive={interactive || undefined}\n      className={cn(\n        \"reveal-host\",\n        styles.root,\n        VARIANT[variant],\n        size === \"sm\" && styles.sm,\n        classNames?.root,\n        className,\n      )}\n    >\n      {href ? (\n        <a\n          href={href}\n          onClick={selectable ? choose : undefined}\n          className={cn(styles.main, classNames?.main)}\n          data-slot=\"item-main\"\n          aria-current={selected ? \"page\" : undefined}\n        >\n          {body}\n        </a>\n      ) : onSelect || selectable ? (\n        <button\n          type=\"button\"\n          onClick={choose}\n          aria-pressed={selected}\n          className={cn(styles.main, classNames?.main)}\n          data-slot=\"item-main\"\n        >\n          {body}\n        </button>\n      ) : (\n        <div className={cn(styles.main, classNames?.main)} data-slot=\"item-main\">\n          {body}\n        </div>\n      )}\n      {actions && (\n        <span\n          ref={tools}\n          data-slot=\"item-actions\"\n          className={cn(styles.actions, !actionsAtRest && \"reveal\", classNames?.actions)}\n        >\n          {actions}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport type ItemIconPalette = \"green\" | \"orange\" | \"red\" | \"blue\" | \"violet\";\nexport type ItemIconTone = \"neutral\" | \"accent\" | \"success\" | \"warning\" | \"danger\" | \"info\" | ItemIconPalette;\nconst ICON_TONES: Record<ItemIconTone, string> = {\n  neutral: \"neutral\",\n  accent: \"accent\",\n  success: \"green\",\n  warning: \"orange\",\n  danger: \"red\",\n  info: \"blue\",\n  green: \"green\",\n  orange: \"orange\",\n  red: \"red\",\n  blue: \"blue\",\n  violet: \"violet\",\n};\n\n/** A tinted tile behind an icon — the default media for items. Its corner follows the look's control radius. */\nexport function ItemIcon({\n  children,\n  tone = \"neutral\",\n  palette,\n  className,\n  ...props\n}: ComponentProps<\"span\"> & {\n  /** Semantic colour; defaults to neutral. @deprecated Named colours should use palette instead. */\n  tone?: ItemIconTone;\n  /** Explicit decorative colour overriding the semantic tone; omitted by default. */\n  palette?: ItemIconPalette;\n}) {\n  useDeprecatedProp(\n    \"ItemIcon\",\n    \"tone\",\n    \"a semantic tone or palette for a named colour\",\n    [\"green\", \"orange\", \"red\", \"blue\", \"violet\"].includes(tone),\n  );\n  return (\n    <span\n      {...props}\n      data-slot=\"item-icon\"\n      data-tone={palette ?? ICON_TONES[tone]}\n      className={cn(styles.icon, className)}\n    >\n      {children}\n    </span>\n  );\n}\n\nexport default Item;\n"},{"path":"beamline/components/kbd/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/kbd/defaults.ts","content":"/** Pure allowed presentation defaults for Kbd; no component or runtime imports. */\nexport interface KbdDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/kbd/kbd.module.css","type":"registry:file","target":"src/ui/beamline/components/kbd/kbd.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Keycaps: each key is a small raised control (the control role), so it reads as a key in every look. */\n  .keys {\n    display: inline-flex;\n    align-items: center;\n    gap: var(--pui-space-0-5);\n    vertical-align: middle;\n    /* a key combination is a fixed sequence: it reads Ctrl K in any page direction, isolated as one unit while the\n       page keeps its own direction around it */\n    direction: ltr;\n    unicode-bidi: isolate;\n  }\n  .cap {\n    display: inline-flex;\n    height: 22px;\n    min-width: 22px;\n    align-items: center;\n    justify-content: center;\n    padding-inline: var(--pui-space-1-5);\n    /* nesting input (O1 §14.1, as Button): a parent that holds this near its rounded corner sets --pui-nest-radius and\n       --pui-nest-inset; the corner becomes radius − inset, clamped between a floor (chip) and the part's own role radius (or --pui-nest-ceiling: a pill container sets it to the pill radius). Unset, the declaration is invalid and the own corner\n       applies. --pui-kbd-radius wins over both. */\n    --_kbd-nest: clamp(\n      calc(var(--pui-radius-chip) / 2),\n      calc(var(--pui-nest-radius) - var(--pui-nest-inset, 0px)),\n      var(--pui-nest-ceiling, var(--pui-radius-chip))\n    );\n    border-radius: var(--pui-kbd-radius, var(--_kbd-nest, calc(var(--pui-radius-chip) - 1px)));\n    color: var(--pui-ink-2);\n    font-size: var(--pui-text-2xs);\n    font-weight: var(--pui-weight-medium);\n    transition:\n      translate var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      var(--m-transition);\n  }\n  /* live: a held key presses its cap down at once (the look's own press light) and lets it up over the leave\n     duration; with every key of the combination held, the caps ink in the accent and take an accent ring */\n  .cap[data-pressed] {\n    --pui-m-press: 1;\n    translate: 0 1px;\n    color: var(--pui-ink);\n    transition-duration: 0ms;\n  }\n  .keys[data-complete] .cap {\n    color: var(--pui-accent-ink);\n    box-shadow:\n      var(--m-control-edge),\n      0 0 0 1px var(--pui-accent);\n  }\n  /* a finished step of a sequence keeps its caps inked while the next is awaited */\n  .cap[data-done] {\n    color: var(--pui-accent-ink);\n  }\n  /* \"then\" between the steps of a sequence: a word, seen and spoken, so G then A never reads as G and A held together.\n     While the next step is awaited a line under it runs out over the time it has (SEQUENCE_MS in kbd.tsx): a timer,\n     so reduced motion keeps it, as HoldToConfirm keeps its fill. */\n  .then {\n    position: relative;\n    padding-inline: var(--pui-space-0-5);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-2xs);\n    font-weight: var(--pui-weight-normal);\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .then[data-waiting] {\n    color: var(--pui-ink-2);\n    transition-duration: 0ms;\n  }\n  .then[data-waiting]::after {\n    content: \"\";\n    position: absolute;\n    inset-inline: var(--pui-space-0-5);\n    bottom: -2px;\n    height: 1px;\n    background: var(--pui-accent);\n    transform-origin: left;\n    /* the same wait the sequence listener keeps (SEQUENCE_MS), passed in so the line and the timeout never drift */\n    animation: kbd-wait var(--_wait) linear forwards;\n  }\n  @keyframes kbd-wait {\n    from {\n      scale: 1 1;\n    }\n    to {\n      scale: 0 1;\n    }\n  }\n  .sm {\n    height: 18px;\n    min-width: 18px;\n    padding-inline: var(--pui-space-1);\n    font-size: var(--pui-text-2xs);\n  }\n  .lg {\n    height: 28px;\n    min-width: 28px;\n    padding-inline: var(--pui-space-2);\n    font-size: var(--pui-text-sm);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.cap {\n  composes: m-control from global;\n}\n"},{"path":"beamline/components/kbd/kbd.tsx","type":"registry:file","target":"src/ui/beamline/components/kbd/kbd.tsx","content":"\"use client\";\n\nimport { useMessages, type Translator } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { KbdDefaults } from \"./defaults\";\nexport type { KbdDefaults } from \"./defaults\";\n\nimport { Fragment, useEffect, useReducer } from \"react\";\nimport type { CSSProperties, HTMLAttributes, ReactNode, Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { useApplePlatform } from \"../../hooks/use-apple-platform\";\nimport styles from \"./kbd.module.css\";\n\nconst MAP_MAC = (i18n: Translator<typeof englishMessages>): Record<string, string> => ({\n  mod: \"⌘\",\n  cmd: \"⌘\",\n  ctrl: \"⌃\",\n  alt: \"⌥\",\n  option: \"⌥\",\n  shift: \"⇧\",\n  enter: \"↵\",\n  backspace: \"⌫\",\n  esc: i18n(\"escapeShort\"),\n  tab: \"⇥\",\n  up: \"↑\",\n  down: \"↓\",\n  left: \"←\",\n  right: \"→\",\n  space: i18n(\"space\"),\n});\nconst MAP_PC = (i18n: Translator<typeof englishMessages>): Record<string, string> => ({\n  mod: i18n(\"controlShort\"),\n  cmd: i18n(\"controlShort\"),\n  ctrl: i18n(\"controlShort\"),\n  alt: i18n(\"alt\"),\n  option: i18n(\"alt\"),\n  shift: i18n(\"shift\"),\n  enter: i18n(\"enter\"),\n  backspace: i18n(\"backspace\"),\n  esc: i18n(\"escapeShort\"),\n  tab: i18n(\"tab\"),\n  up: \"↑\",\n  down: \"↓\",\n  left: \"←\",\n  right: \"→\",\n  space: i18n(\"space\"),\n});\nconst SPOKEN = (i18n: Translator<typeof englishMessages>): Record<string, string> => ({\n  \"⌘\": i18n(\"command\"),\n  \"⌃\": i18n(\"control\"),\n  \"⌥\": i18n(\"option\"),\n  \"⇧\": i18n(\"shift\"),\n  \"↵\": i18n(\"enter\"),\n  \"⌫\": i18n(\"backspace\"),\n  \"⇥\": i18n(\"tab\"),\n  \"↑\": i18n(\"up\"),\n  \"↓\": i18n(\"down\"),\n  \"←\": i18n(\"left\"),\n  \"→\": i18n(\"right\"),\n});\n\n/** The KeyboardEvent.key a cap stands for, compared in lower case. */\nconst EVENT_KEY: Record<string, string> = {\n  ctrl: \"control\",\n  alt: \"alt\",\n  option: \"alt\",\n  shift: \"shift\",\n  enter: \"enter\",\n  backspace: \"backspace\",\n  esc: \"escape\",\n  tab: \"tab\",\n  up: \"arrowup\",\n  down: \"arrowdown\",\n  left: \"arrowleft\",\n  right: \"arrowright\",\n  space: \" \",\n};\nconst eventKey = (k: string, mac: boolean) => {\n  const name = k.toLowerCase();\n  if (name === \"mod\" || name === \"cmd\") return mac ? \"meta\" : \"control\";\n  return EVENT_KEY[name] ?? name;\n};\nconst NONE: ReadonlySet<string> = new Set();\n/** The key between the steps of a sequence: [\"g\", \"then\", \"a\"] is G, then A. */\nconst THEN = \"then\";\n/** How long a sequence waits for its next step, as apps that bind them do. */\nconst SEQUENCE_MS = 1500;\n\n/** live: the keys of the shortcut held now, the step being waited for, whether the whole shortcut is done, and how\n *  many steps have been done (a step done again, G pressed twice, gives the next one its full time again). */\ninterface Live {\n  held: ReadonlySet<string>;\n  step: number;\n  complete: boolean;\n  done: number;\n}\ntype LiveAction =\n  { type: \"down\"; key: string; steps: string[][] } | { type: \"up\"; key: string; steps: string[][] } | { type: \"reset\" };\nconst REST: Live = { held: NONE, step: 0, complete: false, done: 0 };\n\n/** A step is done by the press that completes it: every key of the step held, the last one just pressed. */\nconst advance = (live: Live, key: string, steps: string[][]): Live => {\n  const want = steps[live.step];\n  if (!want.includes(key) || !want.every((k) => live.held.has(k))) return live;\n  return live.step === steps.length - 1\n    ? { ...live, complete: true }\n    : { ...live, step: live.step + 1, done: live.done + 1 };\n};\nfunction track(live: Live, action: LiveAction): Live {\n  if (action.type === \"reset\") return live === REST ? live : REST;\n  const { key, steps } = action;\n  if (action.type === \"down\") {\n    // a key outside the shortcut breaks a sequence half-way; a combination simply waits for its own keys\n    if (!steps.some((s) => s.includes(key))) return live.step || live.complete ? { ...REST, held: live.held } : live;\n    const held = live.held.has(key) ? live.held : new Set(live.held).add(key);\n    const next = advance({ ...live, held }, key, steps);\n    if (next.step === live.step && !next.complete && live.step > 0 && !steps[live.step].includes(key))\n      // a wrong key mid-sequence starts it over, and may itself begin it\n      return advance({ ...live, held, step: 0, complete: false }, key, steps);\n    return next;\n  }\n  // with ⌘ held a Mac sends no keyup for the other keys: letting go of ⌘ lets go of all of them\n  let held = live.held;\n  if (key === \"meta\") held = NONE;\n  else if (held.has(key)) {\n    const next = new Set(held);\n    next.delete(key);\n    held = next;\n  }\n  // letting go of the last step ends the shortcut; an earlier step stays done while the next is awaited\n  if (live.complete && steps[live.step].some((k) => !held.has(k))) return { ...REST, held };\n  return held === live.held ? live : { ...live, held };\n}\n\n/** Typing a letter into a field is typing, not a shortcut. */\nconst typing = (e: KeyboardEvent) => {\n  const t = e.target as HTMLElement | null;\n  return (\n    e.key.length === 1 &&\n    !e.metaKey &&\n    !e.ctrlKey &&\n    !e.altKey &&\n    !!t &&\n    (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))\n  );\n};\n\nexport interface KbdClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the cap part; no override by default. */\n  cap?: string;\n  /** Classes for the symbol part; no override by default. */\n  symbol?: string;\n  /** Classes for the spoken part; no override by default. */\n  spoken?: string;\n  /** The word between the steps of a sequence. */\n  then?: string;\n}\n\nexport interface KbdProps extends HTMLAttributes<HTMLElement> {\n  /** Keys in order; held together they make a combination: `[\"mod\", \"K\"]` renders ⌘K on Mac and Ctrl K elsewhere.\n   *  `\"then\"` between keys makes a sequence: `[\"g\", \"then\", \"a\"]` is G, then A. */\n  keys?: string[];\n  /** The caps answer the keyboard: each presses down while the viewer holds its key, and the combination lights when\n   *  all are held; in a sequence each finished step stays lit while the next is awaited (1.5 s). For teaching a\n   *  shortcut where it is shown; it listens only while mounted with `live`. */\n  live?: boolean;\n  /** Platform used to name modifier keys; defaults to the viewer's detected platform. */\n  platform?: \"mac\" | \"other\";\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: KbdClassNames;\n}\n\n/**\n * Keyboard keys as keycaps, for combinations (⌘K) and sequences (G then A). `mod` resolves to ⌘ or Ctrl for the\n * viewer's platform; symbols are spoken as words for screen readers. Pass `children` for a single literal key.\n */\nexport function Kbd({\n  keys,\n  live = false,\n  platform,\n  size: sizeProp,\n  className,\n  classNames,\n  children,\n  ref,\n  ...props\n}: KbdProps) {\n  const i18n = useMessages(\"kbd\", englishMessages);\n\n  const scopeDefaults = useComponentDefaults<KbdDefaults>(\"kbd\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const detectedMac = useApplePlatform();\n  const mac = platform === undefined ? detectedMac : platform === \"mac\";\n  const map = mac ? MAP_MAC(i18n) : MAP_PC(i18n);\n  const spoken = SPOKEN(i18n);\n  // the steps: one for a combination, one per \"then\" for a sequence (a stray \"then\" at either end is no step)\n  const parts: string[][] = [[]];\n  for (const k of keys ?? []) {\n    if (k.toLowerCase() === THEN) parts.push([]);\n    else parts[parts.length - 1].push(k);\n  }\n  const steps = parts.filter((s) => s.length > 0);\n  // live: which of the caps' keys the viewer holds now, and how far through the sequence they are\n  const [state, dispatch] = useReducer(track, REST);\n  const wanted = live && keys ? steps.map((s) => s.map((k) => eventKey(k, mac)).join(\"\\u0000\")).join(\"\\u0001\") : \"\";\n  useEffect(() => {\n    if (!wanted) return;\n    const events = wanted.split(\"\\u0001\").map((s) => s.split(\"\\u0000\"));\n    const press = (e: KeyboardEvent) => {\n      if (e.repeat || typing(e)) return;\n      dispatch({ type: \"down\", key: e.key.toLowerCase(), steps: events });\n    };\n    const release = (e: KeyboardEvent) => dispatch({ type: \"up\", key: e.key.toLowerCase(), steps: events });\n    const clear = () => dispatch({ type: \"reset\" });\n    window.addEventListener(\"keydown\", press);\n    window.addEventListener(\"keyup\", release);\n    window.addEventListener(\"blur\", clear);\n    return () => {\n      window.removeEventListener(\"keydown\", press);\n      window.removeEventListener(\"keyup\", release);\n      window.removeEventListener(\"blur\", clear);\n      dispatch({ type: \"reset\" });\n    };\n  }, [wanted]);\n  // a sequence waits for its next step a moment, then starts over\n  const waiting = state.step > 0 && !state.complete;\n  useEffect(() => {\n    if (!waiting) return;\n    const t = window.setTimeout(() => dispatch({ type: \"reset\" }), SEQUENCE_MS);\n    return () => window.clearTimeout(t);\n  }, [waiting, state.done]);\n  const label = (k: string) => map[k.toLowerCase()] ?? (k.length === 1 ? k.toUpperCase() : k);\n  const cap = cn(styles.cap, \"font-sans\", size !== \"md\" && styles[size], classNames?.cap);\n  const face = (k: ReactNode) =>\n    // a label on a plain span is not read; the symbol is hidden and its word is spoken instead\n    typeof k === \"string\" && spoken[k] ? (\n      <>\n        <span data-slot=\"kbd-symbol\" className={classNames?.symbol} aria-hidden>\n          {k}\n        </span>\n        <span data-slot=\"kbd-spoken\" className={cn(\"sr-only\", classNames?.spoken)}>\n          {spoken[k]}\n        </span>\n      </>\n    ) : (\n      k\n    );\n  return (\n    <kbd\n      ref={ref}\n      data-slot=\"kbd\"\n      data-complete={state.complete || undefined}\n      className={cn(styles.keys, classNames?.root, className)}\n      {...props}\n    >\n      {keys ? (\n        steps.map((step, s) => (\n          <Fragment key={s}>\n            {s > 0 && (\n              <span\n                // a new wait restarts the line under it\n                key={waiting && s === state.step ? `then-${state.done}` : \"then\"}\n                data-slot=\"kbd-then\"\n                data-waiting={(waiting && s === state.step) || undefined}\n                className={cn(styles.then, classNames?.then)}\n                style={{ \"--_wait\": `${SEQUENCE_MS}ms` } as CSSProperties}\n              >\n                {i18n(\"then\")}\n              </span>\n            )}\n            {step.map((k, i) => {\n              const event = eventKey(k, mac);\n              return (\n                <span\n                  data-slot=\"kbd-cap\"\n                  key={i}\n                  data-pressed={(live && s === state.step && state.held.has(event)) || undefined}\n                  data-done={(live && s < state.step) || undefined}\n                  className={cap}\n                >\n                  {face(label(k))}\n                </span>\n              );\n            })}\n          </Fragment>\n        ))\n      ) : (\n        <span data-slot=\"kbd-cap\" className={cap}>\n          {face(children)}\n        </span>\n      )}\n    </kbd>\n  );\n}\n\nexport default Kbd;\n"},{"path":"beamline/components/kbd/messages.ts","type":"registry:file","target":"src/ui/beamline/components/kbd/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  then: \"then\",\n  escapeShort: \"Esc\",\n  space: \"Space\",\n  controlShort: \"Ctrl\",\n  alt: \"Alt\",\n  shift: \"Shift\",\n  enter: \"Enter\",\n  backspace: \"Backspace\",\n  tab: \"Tab\",\n  command: \"Command\",\n  control: \"Control\",\n  option: \"Option\",\n  up: \"Up\",\n  down: \"Down\",\n  left: \"Left\",\n  right: \"Right\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/label/label.tsx","type":"registry:file","target":"src/ui/beamline/components/label/label.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport type { ComponentProps } from \"react\";\nimport * as RadixLabel from \"@radix-ui/react-label\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface LabelProps extends ComponentProps<typeof RadixLabel.Root> {\n  /** Shows the marker and announces \"required\". */\n  required?: boolean;\n  /** The control says \"required\" itself (a native required input, a radiogroup with aria-required): the label shows\n   * the marker only, so the control's name stays the label's words and the state is announced once. */\n  requiredOnControl?: boolean;\n}\n\n/** The text label tied to a form control. It dims with a disabled control next to it (peer) or inside it (group). */\nexport function Label({ required = false, requiredOnControl = false, className, children, ...props }: LabelProps) {\n  const i18n = useMessages(\"label\", englishMessages);\n\n  return (\n    <RadixLabel.Root\n      data-slot=\"label\"\n      {...props}\n      className={cn(\n        \"inline-flex items-center gap-[var(--pui-space-1)] text-sm leading-tight font-medium text-ink select-none group-data-[disabled]:opacity-[var(--pui-opacity-disabled)] peer-disabled:opacity-[var(--pui-opacity-disabled)]\",\n        className,\n      )}\n    >\n      {children}\n      {required && (\n        <>\n          <span aria-hidden className=\"text-red\">\n            *\n          </span>\n          {!requiredOnControl && <span className=\"sr-only\">{i18n(\"required\")}</span>}\n        </>\n      )}\n    </RadixLabel.Root>\n  );\n}\n\nexport default Label;\n"},{"path":"beamline/components/label/messages.ts","type":"registry:file","target":"src/ui/beamline/components/label/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  required: \"required\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/pagination/messages.ts","type":"registry:file","target":"src/ui/beamline/components/pagination/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: \"Pagination\",\n  noResults: \"No results\",\n  of: \"{value}–{value2} of {value3}\",\n  previousPage: \"Previous page\",\n  page: \"Page {p}\",\n  of2: \"{page} of {pageCount}\",\n  page2: \"Page\",\n  nextPage: \"Next page\",\n  pageTotalUnknown: \"{page} · total unknown\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/pagination/pagination.tsx","type":"registry:file","target":"src/ui/beamline/components/pagination/pagination.tsx","content":"\"use client\";\nimport { useListWindow } from \"../_shared/list-state\";\nimport { useMessages, numberFormatter } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useLocale } from \"../../runtime/localization\";\n\nimport { useRef, type CSSProperties, type Ref } from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { Button } from \"../button/button\";\nimport { cn } from \"../../lib/utils\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\nimport { useFlip } from \"../../hooks/use-flip\";\nimport { useIndicator } from \"../../hooks/use-indicator\";\nimport { useContour } from \"../../hooks/use-contour\";\n\nexport interface PaginationClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the summary part; no override by default. */\n  summary?: string;\n  /** Classes for the controls part; no override by default. */\n  controls?: string;\n  /** Classes for the previous part; no override by default. */\n  previous?: string;\n  /** Classes for the list part; no override by default. */\n  list?: string;\n  /** Classes for the gap part; no override by default. */\n  gap?: string;\n  /** Classes for the item part; no override by default. */\n  item?: string;\n  /** Classes for the page part; no override by default. */\n  page?: string;\n  /** Classes for the compact part; no override by default. */\n  compact?: string;\n  /** Classes for the next part; no override by default. */\n  next?: string;\n}\n\nexport interface PaginationProps {\n  /** Controlled current page, counted from 1; omit to use defaultPage. */\n  page?: number;\n  /** Initial page when uncontrolled, counted from 1. Defaults to 1. */\n  defaultPage?: number;\n  /** Number of pages for existing callers; omit when rowCount is supplied. */\n  pageCount?: number;\n  /** Total matching rows; null is unknown. Overrides deprecated total. */\n  rowCount?: number | null;\n  /** Initial rows per page. Default 25 when rowCount is supplied. */\n  defaultPageSize?: number;\n  onPageSizeChange?: (pageSize: number) => void;\n  /** Unknown totals: false disables Next. */\n  hasMore?: boolean;\n  /** Reports page number after a change; no callback by default. */\n  onPageChange?: (page: number) => void;\n  /** Pages shown on each side of the current one. Default 1. */\n  siblings?: number;\n  /** With both, a line like \"41–60 of 468\" sits before the pages. */\n  pageSize?: number;\n  /** @deprecated Use rowCount. */\n  total?: number | null;\n  /** The landmark's name. Default \"Pagination\". */\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?: PaginationClassNames;\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/** The page numbers to show: always the same number of slots (first, last, the current with its siblings, and an\n *  ellipsis for each gap), so Next and Previous never move under the pointer while paging. With few enough pages every\n *  page shows; an ellipsis always stands for at least two pages. */\nexport function pageWindow(page: number, count: number, siblings = 1): (number | \"gap\")[] {\n  const range = (a: number, b: number) => Array.from({ length: Math.max(0, b - a + 1) }, (_, i) => a + i);\n  const slots = 2 * siblings + 5;\n  if (count <= slots) return range(1, count);\n  const left = Math.max(page - siblings, 1);\n  const right = Math.min(page + siblings, count);\n  if (left <= 3) return [...range(1, slots - 2), \"gap\", count];\n  if (right >= count - 2) return [1, \"gap\", ...range(count - (slots - 3), count)];\n  return [1, \"gap\", ...range(left, right), \"gap\", count];\n}\n\n/** From this room (2xl, 42rem) one more neighbour shows on each side of the current page. */\nconst WIDE = 672;\n\n/** Moves through pages of a known length. It answers to its own room, not the window: narrow, the numbers give way to\n *  \"Page 6 of 24\" and the summary keeps to one line; wide, one more neighbour shows on each side of the current page. */\nexport function Pagination({\n  page: pageProp,\n  defaultPage = 1,\n  pageCount: pageCountProp,\n  onPageChange,\n  siblings = 1,\n  pageSize: pageSizeProp,\n  defaultPageSize,\n  onPageSizeChange,\n  rowCount,\n  hasMore,\n  total: totalProp,\n  label: labelProp,\n  className,\n  classNames,\n  style,\n  ref,\n}: PaginationProps) {\n  const i18n = useMessages(\"pagination\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n\n  const locale = useLocale();\n\n  const paging = useListWindow({\n    page: pageProp,\n    defaultPage,\n    onPageChange,\n    pageSize: pageSizeProp,\n    defaultPageSize,\n    onPageSizeChange,\n    rowCount,\n    total: totalProp,\n  });\n  const total = paging.rowCount;\n  const pageSize =\n    pageSizeProp ?? (rowCount !== undefined || defaultPageSize !== undefined ? paging.pageSize : undefined);\n  const pageCount =\n    total === null\n      ? null\n      : total != null && pageSize\n        ? Math.max(1, Math.ceil(total / pageSize))\n        : (pageCountProp ?? null);\n  const page = pageProp === undefined ? Math.min(Math.max(1, pageCount ?? Infinity), paging.page) : paging.page;\n  const root = useRef<HTMLElement>(null);\n  const attach = useComposedRef(root, ref);\n  const wide = useWidthAs(root, (w) => w >= WIDE, false);\n  // the numbers show while the room holds them: every slot of the window and the two arrows at the arrows' own size\n  // (so the type and spacing scales count), and the range summary's words when there is one; else \"Page 6 of 24\"\n  const prev = useRef<HTMLButtonElement>(null);\n  const summary = useRef<HTMLSpanElement>(null);\n  const slots = pageWindow(page, Math.max(pageCount ?? page, 1), siblings + (wide ? 1 : 0)).length;\n  const fits = useWidthAs(\n    root,\n    (w) => {\n      const unit = (prev.current?.offsetWidth || 32) + 2;\n      const words = summary.current ? summary.current.scrollWidth + 12 : 0;\n      return w === 0 || w >= (slots + 2) * unit + words;\n    },\n    true,\n    `${slots}:${pageSize ?? \"\"}:${total ?? \"\"}`,\n  );\n  // the current page's plate travels to the new page as matter (ui/lib/contour.ts) instead of one button lighting up\n  const [list, current] = useIndicator<HTMLOListElement>(\"[aria-current=page]\", `${page}:${pageCount}:${wide}`);\n  const plate = useContour<HTMLLIElement>(current);\n  // the numbers keep their path: when the window moves along, the ones that stay slide to their new slot under the\n  // plate, which stays in the middle, and the one arriving grows in. While the room shows \"Page 6 of 24\" the numbers\n  // are not laid out: nothing is read, so they never slide in from nowhere when the room grows back\n  useFlip(() => (list.current?.offsetWidth ? list.current : null), { ghost: false });\n  if (pageCount !== null && pageCount < 1) return null;\n  const go = (p: number) => {\n    const next = Math.min(pageCount ?? Infinity, Math.max(1, p));\n    paging.setPage(next);\n  };\n  const from = pageSize && total != null ? (page - 1) * pageSize + 1 : null;\n  return (\n    <nav\n      aria-label={label}\n      data-slot=\"pagination\"\n      className={cn(\"@container flex w-full items-center gap-3\", classNames?.root, className)}\n      style={style}\n      ref={attach}\n    >\n      {from !== null && total != null && (\n        <span\n          ref={summary}\n          data-slot=\"pagination-summary\"\n          // a range in words reads in its own direction (\"101–120 of 468\"), whatever the page around it\n          dir=\"auto\"\n          className={cn(\"me-auto min-w-0 truncate text-xs text-ink-3 tabular-nums\", classNames?.summary)}\n        >\n          {total === 0\n            ? i18n(\"noResults\")\n            : i18n(\"of\", {\n                value: numberFormatter(locale).format(from),\n                value2: numberFormatter(locale).format(Math.min(total, page * (pageSize ?? 0))),\n                value3: numberFormatter(locale).format(total),\n              })}\n        </span>\n      )}\n      <div data-slot=\"pagination-controls\" className={cn(\"flex items-center gap-1\", classNames?.controls)}>\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          disabled={page <= 1}\n          onClick={() => go(page - 1)}\n          ref={prev}\n          aria-label={i18n(\"previousPage\")}\n          data-slot=\"pagination-previous\"\n          className={classNames?.previous}\n        >\n          <ChevronLeft className=\"rtl:rotate-180\" />\n        </Button>\n        <ol\n          ref={list}\n          data-slot=\"pagination-list\"\n          className={cn(\n            \"relative isolate items-center gap-0.5\",\n            fits && pageCount !== null ? \"flex\" : \"hidden\",\n            classNames?.list,\n          )}\n        >\n          {current && (\n            <li\n              ref={plate}\n              aria-hidden\n              data-slot=\"pagination-indicator\"\n              className=\"m-control absolute top-0 left-0 -z-10 rounded-control\"\n            />\n          )}\n          {pageWindow(page, pageCount ?? page, siblings + (wide ? 1 : 0)).map((p, i) =>\n            p === \"gap\" ? (\n              <li\n                key={`gap-${i}`}\n                aria-hidden\n                data-slot=\"pagination-gap\"\n                className={cn(\"grid w-8 place-items-center text-sm text-ink-3\", classNames?.gap)}\n              >\n                …\n              </li>\n            ) : (\n              <li data-slot=\"pagination-item\" className={classNames?.item} key={p}>\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  data-slot=\"pagination-page\"\n                  // the number slides, not its slot: a moving slot would become the plate's offset parent mid-slide\n                  data-flip={p}\n                  aria-current={p === page ? \"page\" : undefined}\n                  aria-label={i18n(\"page\", { p: p })}\n                  onClick={() => go(p)}\n                  className={cn(\n                    \"min-w-8 px-2 font-numeric tabular-nums\",\n                    p === page ? \"text-ink\" : \"text-ink-2\",\n                    classNames?.page,\n                  )}\n                >\n                  <span>{p}</span>\n                </Button>\n              </li>\n            ),\n          )}\n        </ol>\n        <span\n          data-slot=\"pagination-compact\"\n          className={cn(\n            \"px-2 text-sm whitespace-nowrap text-ink-2 tabular-nums\",\n            fits && pageCount !== null && \"hidden\",\n            classNames?.compact,\n          )}\n          aria-live=\"polite\"\n        >\n          <span className=\"hidden @3xs:inline\">{i18n(\"page2\")} </span>\n          {pageCount === null ? i18n(\"pageTotalUnknown\", { page }) : i18n(\"of2\", { page, pageCount })}\n        </span>\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          disabled={pageCount === null ? hasMore === false : page >= pageCount}\n          onClick={() => go(page + 1)}\n          aria-label={i18n(\"nextPage\")}\n          data-slot=\"pagination-next\"\n          className={classNames?.next}\n        >\n          <ChevronRight className=\"rtl:rotate-180\" />\n        </Button>\n      </div>\n    </nav>\n  );\n}\n\nexport default Pagination;\n"},{"path":"beamline/components/radio-cards/radio-cards.module.css","type":"registry:file","target":"src/ui/beamline/components/radio-cards/radio-cards.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Radio cards: raised option cards (the control role) with one accent ring that slides to the chosen card. */\n  /* the grid takes the columns its room allows (radio-cards.tsx arranges: one row, a balanced grid, or a list) */\n  .group {\n    position: relative;\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(min(var(--pui-radio-cards-card-width, 180px), 100%), 1fr));\n    gap: var(--pui-space-2);\n  }\n  .group[data-columns] {\n    grid-template-columns: repeat(var(--_cols), minmax(0, 1fr));\n  }\n  .list {\n    grid-template-columns: 1fr;\n  }\n  .row {\n    display: flex;\n    flex-wrap: nowrap;\n    overflow-x: auto;\n    /* Keep a full focus outline inside the scroller without changing the cards' layout footprint. */\n    margin: calc(var(--pui-space-1) * -1);\n    padding: var(--pui-space-1);\n  }\n  .row .card {\n    flex: 0 0 var(--pui-radio-cards-card-width, 180px);\n  }\n  /* placed and moved by glide (utilities.css), on both axes: it stretches toward the chosen card and gathers there */\n  .ring {\n    position: absolute;\n    z-index: 1;\n    border-radius: var(--pui-radius-card);\n    box-shadow: inset 0 0 0 1.5px var(--pui-accent);\n    pointer-events: none;\n  }\n  /* the ring before it is first placed (a page drawn on a server): the chosen card wears it */\n  .placing::after {\n    content: \"\";\n    position: absolute;\n    inset: 0;\n    z-index: 1;\n    border-radius: inherit;\n    box-shadow: inset 0 0 0 1.5px var(--pui-accent);\n    pointer-events: none;\n  }\n  .card {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    min-width: 0;\n    gap: var(--pui-space-3);\n    padding: var(--pui-space-3);\n    border-radius: var(--pui-radius-card);\n    text-align: start;\n    transition: var(--m-transition);\n  }\n  .card:hover {\n    transition-duration: 0ms;\n  }\n  /* a wide thing presses shallower than a small one: a third of the small-control depth, at once */\n  .card:active:not(:disabled) {\n    scale: calc(1 - (1 - var(--pui-press-scale)) / 3);\n    transition-duration: 0ms;\n  }\n  .card:disabled {\n    opacity: var(--pui-opacity-disabled);\n    pointer-events: none;\n  }\n  .list .card {\n    flex-direction: row;\n    align-items: center;\n  }\n  .media {\n    display: block;\n    width: 100%;\n    aspect-ratio: 16 / 9;\n    flex-shrink: 0;\n    overflow: hidden;\n    border-radius: var(--pui-radius-control);\n  }\n  .media :where(img, video, canvas) {\n    display: block;\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n  }\n  .head {\n    display: flex;\n    width: 100%;\n    align-items: flex-start;\n    gap: var(--pui-space-3);\n  }\n  .list .media {\n    width: min(32%, 120px);\n  }\n  .list .head {\n    flex: 1;\n    min-width: 0;\n    align-items: center;\n  }\n  .icon {\n    display: grid;\n    width: 32px;\n    height: 32px;\n    flex-shrink: 0;\n    place-items: center;\n    /* nested in the card: the chip radius, or the card radius minus the padding when that is rounder (roundness 200%) */\n    --_r: max(0px, var(--pui-radius-card) - var(--pui-space-3) - var(--pui-radius-chip));\n    border-radius: calc(var(--pui-radius-chip) + var(--_r));\n    background: var(--pui-hover-2);\n    color: var(--pui-ink-2);\n  }\n  .icon :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n  .text {\n    display: grid;\n    min-width: 0;\n    flex: 1;\n    gap: var(--pui-space-0-5);\n  }\n  .label {\n    color: var(--pui-ink);\n    font-size: var(--pui-text-base);\n    font-weight: var(--pui-weight-medium);\n  }\n  .detail {\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    line-height: 1.375;\n  }\n  /* the radio mark is RadioGroup's own: the toggle role's well, filled with a dot when chosen */\n  .mark {\n    --m-color: var(--pui-control-on);\n    --m-toggle-off: var(--pui-field);\n    --m-toggle-off-hover: var(--pui-field);\n    --m-toggle-ring: var(--pui-edge-control);\n    --m-toggle-ring-hover: color-mix(in oklab, var(--pui-edge-control) 60%, var(--pui-ink));\n    display: grid;\n    width: 16px;\n    height: 16px;\n    flex-shrink: 0;\n    place-items: center;\n    border-radius: 50%;\n    transition: var(--m-transition);\n  }\n  .card:hover .mark {\n    --m-toggle-ring: var(--m-toggle-ring-hover);\n    transition-duration: 0ms;\n  }\n  .mark[data-state=\"checked\"]::after {\n    content: \"\";\n    width: 6px;\n    height: 6px;\n    border-radius: 50%;\n    background: var(--pui-control-glyph);\n    animation: mark-in var(--pui-duration-fast) var(--pui-ease-enter);\n  }\n  @keyframes mark-in {\n    from {\n      scale: 0.2;\n    }\n  }\n  /* the figure people compare (a price, a size) closes the card under a seam, in the body face with even digits */\n  .figure {\n    margin-top: auto;\n    padding-top: var(--pui-space-2-5);\n    border-top: 1px solid var(--pui-line-soft);\n    color: var(--pui-ink);\n    font-size: var(--pui-text-base);\n    font-weight: var(--pui-weight-medium);\n    font-variant-numeric: tabular-nums;\n  }\n  /* in a row the figure stays whole on one line (\"$24 / seat\"); the detail beside it gives way instead */\n  .list .figure {\n    margin-top: 0;\n    padding-top: 0;\n    border-top: 0;\n    margin-inline-start: auto;\n    font-size: var(--pui-text-sm);\n    white-space: nowrap;\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.ring {\n  composes: glide glide-2d from global;\n}\n.card {\n  composes: m-control from global;\n}\n.mark {\n  composes: m-toggle from global;\n}\n"},{"path":"beamline/components/radio-cards/radio-cards.tsx","type":"registry:file","target":"src/ui/beamline/components/radio-cards/radio-cards.tsx","content":"\"use client\";\n\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { useState, type CSSProperties, type ReactNode, type Ref } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport * as RadioGroup from \"@radix-ui/react-radio-group\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\nimport { glideProps, useIndicator } from \"../../hooks/use-indicator\";\nimport { cn } from \"../../lib/utils\";\nimport { ChoiceField, choiceIsField, type ChoiceFieldAria } from \"../_shared/field\";\nimport styles from \"./radio-cards.module.css\";\n\nexport interface RadioCard {\n  /** Value represented by this item. Required. */\n  value: string;\n  /** Text displayed for this item. Required. */\n  label: ReactNode;\n  /** One or two lines under the label. */\n  detail?: ReactNode;\n  /** The figure people compare: a price, a size. */\n  figure?: ReactNode;\n  /** An image or other preview. Keep interactive controls outside the radio card. */\n  media?: ReactNode;\n  /** Icon element displayed with this item. Omitted by default. */\n  icon?: ReactNode;\n  /** Whether this option is unavailable for selection. Omitted by default. */\n  disabled?: boolean;\n}\n\nexport type RadioCardsLayout = \"grid\" | \"stacked\" | \"row\";\n\n/** How a grid of cards uses its room: all in one row, a balanced grid, or rows of a list when cards side by side would\n *  be a single column of tall cards (or three cards would leave one alone on a second line). */\nfunction arrange(\n  width: number,\n  cards: number,\n  card: number,\n  gap: number,\n): { kind: \"row\" | \"grid\" | \"list\"; cols: number } {\n  const fit = Math.max(1, Math.floor((width + gap) / (card + gap)));\n  if (fit >= cards) return { kind: \"row\", cols: cards };\n  if (fit < 2) return { kind: \"list\", cols: 1 };\n  const rows = Math.ceil(cards / fit);\n  const cols = Math.ceil(cards / rows);\n  if (cards <= 3 && cards % cols === 1) return { kind: \"list\", cols: 1 };\n  return { kind: \"grid\", cols };\n}\nexport type RadioCardsClassNames = Partial<\n  Record<\n    | \"root\"\n    | \"item\"\n    | \"media\"\n    | \"head\"\n    | \"icon\"\n    | \"text\"\n    | \"label\"\n    | \"detail\"\n    | \"mark\"\n    | \"figure\"\n    | \"ring\"\n    | \"field\"\n    | \"fieldLabel\"\n    | \"description\"\n    | \"error\",\n    string\n  >\n>;\n\nexport interface RadioCardsProps {\n  /** Stable choices with optional media, detail, figure and icon. */\n  cards: RadioCard[];\n  /** Controlled value of the chosen card; omit to use defaultValue. */\n  value?: string;\n  /** Initial value of the chosen card when uncontrolled. Defaults to no selected card. */\n  defaultValue?: string;\n  /** Reports value of the chosen card after an edit; no callback by default. */\n  onValueChange?: (value: string) => void;\n  /** Stack the cards as rows instead of setting them side by side. */\n  stacked?: boolean;\n  /** Grid by default; row keeps one nonwrapping horizontal row. Overrides the stacked shorthand. */\n  layout?: RadioCardsLayout;\n  /** Grid minimum or row card width, in px. Defaults to inherited --pui-radio-cards-card-width, then 180. */\n  cardWidth?: number;\n  /** The group's name or question (e.g. \"Plan\"), shown above it like every field's label; `hideLabel` keeps it for\n   *  assistive tech only. */\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 cards (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  /** Field name used in native form submission. Defaults to no form field name. */\n  name?: string;\n  /** Marks the field as required for form submission. Defaults to false. */\n  required?: boolean;\n  /** Prevents interaction with the control. Defaults to false. */\n  disabled?: boolean;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the documented visual parts; keyboard behavior and measured ring remain owned by the control. */\n  classNames?: RadioCardsClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the radio group, preserving its measurement and selection refs; omitted by default. */\n  ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * One choice among a few rich options: plans, regions, instance sizes. A single selection ring slides to the chosen\n * card; arrows move and choose, like any radio group.\n */\nexport function RadioCards({\n  cards,\n  value,\n  defaultValue,\n  onValueChange,\n  stacked = false,\n  layout: layoutProp,\n  cardWidth,\n  label,\n  field,\n  hideLabel: hideLabelProp,\n  showLabel,\n  description,\n  error,\n  name,\n  required,\n  disabled,\n  className,\n  classNames,\n  style,\n  ref,\n}: RadioCardsProps) {\n  useDeprecatedProp(\"RadioCards\", \"hideLabel\", \"showLabel={false}\", hideLabelProp !== undefined);\n  const hideLabel = showLabel === undefined ? hideLabelProp : !showLabel;\n  const layout = layoutProp ?? (stacked ? \"stacked\" : \"grid\");\n  const [current, setCurrent] = useState(value ?? defaultValue ?? null);\n  const chosen = value !== undefined ? value : current;\n  const [group, ring] = useIndicator<HTMLDivElement>(\n    \"[role=radio][data-state=checked]\",\n    `${chosen}:${layout}:${cards.length}`,\n  );\n  const attach = useComposedRef(group, ref);\n  // a grid answers to its room: the card width (prop, inherited token, or 180) and the gap are read from the group\n  const fitted = useWidthAs(\n    group,\n    (w) => {\n      const el = group.current;\n      const cs = el ? getComputedStyle(el) : null;\n      const card = cardWidth ?? (parseFloat(cs?.getPropertyValue(\"--pui-radio-cards-card-width\") ?? \"\") || 180);\n      const gap = parseFloat(cs?.columnGap ?? \"\") || 8;\n      const a = arrange(w, cards.length, card, gap);\n      return `${a.kind}:${a.cols}`;\n    },\n    // before it is measured (a page drawn on a server) the grid's own CSS fits the cards to the room\n    \"grid:\",\n    // a different number of cards or card width is a different question\n    `${cards.length}:${cardWidth ?? \"\"}`,\n  );\n  const [arrangement, cols] = fitted.split(\":\") as [\"row\" | \"grid\" | \"list\", string];\n  const listed = layout === \"stacked\" || (layout === \"grid\" && arrangement === \"list\");\n  const geometry =\n    cardWidth === undefined ? undefined : ({ \"--pui-radio-cards-card-width\": `${cardWidth}px` } as CSSProperties);\n  const asField = choiceIsField(field, label, hideLabel, description != null || error != null || !!required);\n  const control = (aria?: ChoiceFieldAria) => (\n    <RadioGroup.Root\n      ref={attach}\n      data-slot=\"radio-cards\"\n      data-layout={layout}\n      data-arrangement={layout === \"grid\" ? arrangement : undefined}\n      data-columns={layout === \"grid\" && !listed && cols ? cols : undefined}\n      value={value ?? undefined}\n      defaultValue={defaultValue ?? undefined}\n      onValueChange={(v) => {\n        setCurrent(v);\n        onValueChange?.(v);\n      }}\n      name={name}\n      required={required}\n      disabled={disabled}\n      aria-label={aria ? undefined : label}\n      {...aria}\n      className={cn(\n        styles.group,\n        listed && styles.list,\n        layout === \"row\" && styles.row,\n        classNames?.root,\n        !asField && className,\n      )}\n      style={{\n        ...geometry,\n        ...(layout === \"grid\" && !listed && cols ? ({ \"--_cols\": cols } as CSSProperties) : null),\n        ...(asField ? null : style),\n      }}\n    >\n      {ring && (\n        <span\n          aria-hidden\n          data-slot=\"radio-cards-ring\"\n          className={cn(styles.ring, classNames?.ring)}\n          {...glideProps(ring)}\n        />\n      )}\n      {cards.map((o) => (\n        <RadioGroup.Item\n          key={o.value}\n          value={o.value}\n          data-value={o.value}\n          data-slot=\"radio-cards-item\"\n          disabled={o.disabled}\n          // until the ring is first placed (a page drawn on a server), the chosen card draws it itself\n          className={cn(styles.card, !ring && o.value === chosen && styles.placing, classNames?.item)}\n        >\n          {o.media != null && (\n            <span data-slot=\"radio-cards-media\" className={cn(styles.media, classNames?.media)}>\n              {o.media}\n            </span>\n          )}\n          <span data-slot=\"radio-cards-head\" className={cn(styles.head, classNames?.head)}>\n            {o.icon && (\n              <span data-slot=\"radio-cards-icon\" className={cn(styles.icon, classNames?.icon)}>\n                {o.icon}\n              </span>\n            )}\n            <span data-slot=\"radio-cards-text\" className={cn(styles.text, classNames?.text)}>\n              <span data-slot=\"radio-cards-label\" className={cn(styles.label, classNames?.label)}>\n                {o.label}\n              </span>\n              {o.detail && (\n                <span data-slot=\"radio-cards-detail\" className={cn(styles.detail, classNames?.detail)}>\n                  {o.detail}\n                </span>\n              )}\n            </span>\n            <span\n              aria-hidden\n              data-slot=\"radio-cards-mark\"\n              data-state={chosen === o.value ? \"checked\" : \"unchecked\"}\n              className={cn(styles.mark, classNames?.mark)}\n            />\n          </span>\n          {o.figure && (\n            <span data-slot=\"radio-cards-figure\" className={cn(styles.figure, classNames?.figure)}>\n              {o.figure}\n            </span>\n          )}\n        </RadioGroup.Item>\n      ))}\n    </RadioGroup.Root>\n  );\n  if (!asField) return control();\n  return (\n    <ChoiceField\n      slot=\"radio-cards-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={{ label: classNames?.fieldLabel, description: classNames?.description, error: classNames?.error }}\n    >\n      {control}\n    </ChoiceField>\n  );\n}\n\nexport default RadioCards;\n"},{"path":"beamline/components/search-field/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/search-field/defaults.ts","content":"/** Pure allowed presentation defaults for SearchField; no component or runtime imports. */\nexport interface SearchFieldDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/search-field/messages.ts","type":"registry:file","target":"src/ui/beamline/components/search-field/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  clearSearch: \"Clear search\",\n  searching: \"Searching\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/search-field/search-field.tsx","type":"registry:file","target":"src/ui/beamline/components/search-field/search-field.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SearchFieldDefaults } from \"./defaults\";\nexport type { SearchFieldDefaults } from \"./defaults\";\n\nimport { useEffect, useRef, useState, type InputHTMLAttributes, type ReactNode, type Ref } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { IconAction } from \"../_shared/icon-action\";\nimport {\n  Field,\n  FieldFrame,\n  fieldControlClass,\n  useFieldIds,\n  type FieldClassNames,\n  type FieldSize,\n} from \"../_shared/field\";\nimport { Glow } from \"../glow/glow\";\nimport { Kbd } from \"../kbd/kbd\";\nimport { Spinner } from \"../spinner/spinner\";\n\nexport interface SearchFieldProps extends Omit<\n  InputHTMLAttributes<HTMLInputElement>,\n  \"type\" | \"size\" | \"value\" | \"defaultValue\"\n> {\n  /** Accessible name; hidden by default (the magnifier and placeholder say what the field is). */\n  label: ReactNode;\n  /** Show the label above the field. */\n  showLabel?: boolean;\n  /** Controlled search text; omit to use defaultValue. */\n  value?: string;\n  /** Initial search text when uncontrolled. Defaults to an empty string. */\n  defaultValue?: string;\n  /** Reports search text after an edit; no callback by default. */\n  onValueChange?: (value: string) => void;\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: FieldSize;\n  /** A shortcut hint shown while the field is empty, e.g. \"/\" or \"⌘K\". A one-key shortcut (\"/\") is the field's own: pressed\n   *  anywhere but in a text field, it focuses the search. A chord (\"⌘K\") is shown only; the app binds it. */\n  shortcut?: string;\n  /** A band of light rides the border: \"focus\" while focused, true while results are on their way. */\n  beam?: boolean | \"focus\";\n  /** Results are on their way: the magnifier turns into a spinner (aria-busy); typing goes on. */\n  loading?: boolean;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for documented field parts; root style does not replace native behavior. */\n  classNames?: FieldClassNames & Partial<Record<\"input\" | \"clear\", string>>;\n  /** Receives the native input; no ref by default. */\n  ref?: Ref<HTMLInputElement>;\n}\n\n/** The search entry point: magnifier, clear button, Escape to clear, an optional shortcut hint and a live beam. */\nexport function SearchField({\n  label,\n  showLabel = false,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onChange,\n  size: sizeProp,\n  shortcut,\n  beam,\n  loading,\n  id,\n  className,\n  classNames,\n  style,\n  disabled,\n  onKeyDown,\n  onFocus,\n  onBlur,\n  ref,\n  ...props\n}: SearchFieldProps) {\n  const i18n = useMessages(\"search-field\", englishMessages);\n\n  const scopeDefaults = useComponentDefaults<SearchFieldDefaults>(\"search-field\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const { controlId, aria } = useFieldIds(id);\n  const [text, set] = useControllable(value, defaultValue, onValueChange);\n  const [focused, setFocused] = useState(false);\n  const input = useRef<HTMLInputElement | null>(null);\n  const attach = useComposedRef(input, ref);\n  const frameRef = useRef<HTMLDivElement>(null);\n  // the shortcut hint is the first thing to go when the field is narrow: the words typed matter more\n  const roomy = useWidthAs(frameRef, (w) => w >= 200, true);\n  // a one-key shortcut works where it is shown: pressed outside a text field, it brings the person here\n  useEffect(() => {\n    if (!shortcut || [...shortcut].length !== 1 || disabled) return;\n    const down = (e: KeyboardEvent) => {\n      if (e.key !== shortcut || e.metaKey || e.ctrlKey || e.altKey || e.defaultPrevented) return;\n      const at = e.target instanceof HTMLElement ? e.target : null;\n      if (at?.isContentEditable || at?.closest(\"input, textarea, select\")) return;\n      e.preventDefault();\n      input.current?.focus();\n    };\n    window.addEventListener(\"keydown\", down);\n    return () => window.removeEventListener(\"keydown\", down);\n  }, [shortcut, disabled]);\n  // the magnifier comes back scaled in after a search, never on the first paint\n  const [searched, setSearched] = useState(false);\n  if (loading && !searched) setSearched(true);\n  const trailing = text ? (\n    <IconAction\n      data-slot=\"search-clear\"\n      aria-label={i18n(\"clearSearch\")}\n      disabled={disabled}\n      onClick={() => {\n        set(\"\");\n        input.current?.focus();\n      }}\n      className={cn(\"-me-1 animate-enter-scale\", classNames?.clear)}\n    >\n      <X className=\"size-3.5\" />\n    </IconAction>\n  ) : shortcut && !focused && roomy ? (\n    <Kbd className=\"animate-enter-fade\">{shortcut}</Kbd>\n  ) : null;\n  const frame = (\n    <FieldFrame\n      ref={frameRef}\n      classNames={classNames}\n      size={size}\n      disabled={disabled}\n      leading={\n        // one place for what the field is doing: the magnifier, or the ring while results are on their way\n        loading ? (\n          <Spinner key=\"busy\" size=\"sm\" label={i18n(\"searching\")} className=\"animate-enter-scale\" />\n        ) : (\n          <Search key=\"rest\" className={searched ? \"animate-enter-scale\" : undefined} />\n        )\n      }\n      trailing={trailing}\n      // nesting input (O1 §14.1): the shortcut keycap (22px) rounds concentric with the field — a pill in a pill field\n      className=\"[--pui-nest-inset:calc((var(--_field-height)-1.375rem)/2)] [--pui-nest-radius:var(--pui-field-radius,var(--pui-radius-control))]\"\n    >\n      {\" \"}\n      <input\n        ref={attach}\n        data-slot=\"search-field\"\n        {...aria}\n        type=\"search\"\n        role=\"searchbox\"\n        aria-busy={loading || undefined}\n        value={text}\n        disabled={disabled}\n        autoComplete=\"off\"\n        spellCheck={false}\n        className={cn(fieldControlClass, \"[&::-webkit-search-cancel-button]:hidden\", classNames?.input)}\n        onChange={(e) => {\n          set(e.target.value);\n          onChange?.(e);\n        }}\n        onKeyDown={(e) => {\n          // Escape clears; on an empty field it lets go of the field\n          if (e.key === \"Escape\" && text) {\n            e.preventDefault();\n            set(\"\");\n          } else if (e.key === \"Escape\") e.currentTarget.blur();\n          onKeyDown?.(e);\n        }}\n        onFocus={(e) => {\n          setFocused(true);\n          onFocus?.(e);\n        }}\n        onBlur={(e) => {\n          setFocused(false);\n          onBlur?.(e);\n        }}\n        {...props}\n      />\n    </FieldFrame>\n  );\n  const active = beam === \"focus\" ? focused : !!beam;\n  return (\n    <Field\n      label={label}\n      hideLabel={!showLabel}\n      controlId={controlId}\n      className={className}\n      classNames={classNames}\n      style={style}\n    >\n      {/* the wrapper stays while a beam is wanted at all, even off: swapping it in and out would remount the input and drop focus */}\n      {beam !== undefined ? (\n        <Glow effect=\"ring\" active={active} radius=\"var(--pui-radius-control)\" intensity={0.9}>\n          {frame}\n        </Glow>\n      ) : (\n        frame\n      )}\n    </Field>\n  );\n}\n\nexport default SearchField;\n"},{"path":"beamline/components/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/slot-text/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/slot-text/defaults.ts","content":"/** Pure allowed presentation defaults for SlotText; no component or runtime imports. */\nexport interface SlotTextDefaults {\n  /** Shared align setting; omit to inherit the surrounding scope or the component default. */\n  align?: \"start\" | \"end\";\n}\n"},{"path":"beamline/components/slot-text/slot-text.module.css","type":"registry:file","target":"src/ui/beamline/components/slot-text/slot-text.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A reel stands on the run of digits it is rolling to; once it lands it is quietly reset to the same digit on its\n     middle run (no motion), so the next roll can turn either way without running out of reel. A roll is an animation\n     from the numeral it showed to the one it lands on, in whole numerals (the staircase timing in slot-text.tsx), so it\n     always starts from a whole digit, even when a new value arrives mid-roll; the two names take turns so each change\n     starts afresh. */\n  .reel {\n    position: absolute;\n    inset-inline: 0;\n    top: 0;\n    display: flex;\n    flex-direction: column;\n    animation-fill-mode: both;\n  }\n  .rollA {\n    animation-name: slot-text-roll-a;\n  }\n  .rollB {\n    animation-name: slot-text-roll-b;\n  }\n  /* the inline transform names the target; a landed reel is reset over it */\n  .reel[data-settled] {\n    transform: translateY(calc(var(--_rest) * -1lh)) !important;\n    animation: none !important;\n  }\n\n  /* a new place or a changed sign waits, unseen, for its turn in the ripple (its delay is the animation's length), then\n     is there whole: never half faded over its neighbour */\n  .arrive {\n    animation-name: slot-text-wait;\n    animation-timing-function: step-end;\n  }\n  @keyframes slot-text-roll-a {\n    from {\n      transform: translateY(calc(var(--_from) * -1lh));\n    }\n    to {\n      transform: translateY(calc(var(--_to) * -1lh));\n    }\n  }\n  @keyframes slot-text-roll-b {\n    from {\n      transform: translateY(calc(var(--_from) * -1lh));\n    }\n    to {\n      transform: translateY(calc(var(--_to) * -1lh));\n    }\n  }\n  @keyframes slot-text-wait {\n    from,\n    to {\n      visibility: hidden;\n    }\n  }\n}\n"},{"path":"beamline/components/slot-text/slot-text.tsx","type":"registry:file","target":"src/ui/beamline/components/slot-text/slot-text.tsx","content":"\"use client\";\n\nimport { type AnimationEvent, type Ref } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\n\nimport { useLocale, numberFormatter } from \"../../runtime/localization\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SlotTextDefaults } from \"./defaults\";\nexport type { SlotTextDefaults } from \"./defaults\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./slot-text.module.css\";\n\nexport interface SlotTextClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the spoken part; no override by default. */\n  spoken?: string;\n  /** Classes for the visual part; no override by default. */\n  visual?: string;\n  /** Classes for the digit part; no override by default. */\n  digit?: string;\n  /** Classes for the measure part; no override by default. */\n  measure?: string;\n  /** Classes for the reel part; no override by default. */\n  reel?: string;\n  /** Classes for the numeral part; no override by default. */\n  numeral?: string;\n  /** Classes for the character part; no override by default. */\n  character?: string;\n}\n\nexport interface SlotTextProps {\n  /** Text or number shown on the animated reels. Required. */\n  value: string | number;\n  /** Turns a number into its text: currency, thousands. */\n  format?: (value: number) => string;\n  /** ms per reel. Default: twice the considered duration (about 0.7 s), following the motion scale. */\n  duration?: number;\n  /** ms between reels, right to left. Default: the word stagger (about 30 ms), following the motion scale. */\n  stagger?: number;\n  /** Extra full turns a changing digit makes. Default 0. */\n  spins?: number;\n  /** Which side stays put when the length changes. Default end (numbers). */\n  align?: \"start\" | \"end\";\n  /** Read the new value aloud when it changes. Default false (most counters are glanceable). */\n  announce?: boolean;\n  /** Content direction. Numbers stay LTR; other text resolves its own direction instead of inheriting page layout. */\n  dir?: \"ltr\" | \"rtl\" | \"auto\";\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?: SlotTextClassNames;\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}\n\nconst DIGITS = \"0123456789\";\n\n/** A roll of `n` numerals as a staircase on the system's ease-out: each numeral is shown whole and held until the eased\n *  travel reaches the next, so the reel slows into place like an odometer and no frame ever shows two digits split by\n *  the window (the owner rejected digits drawn over each other mid-roll, 27 Oct). */\nconst stairs = new Map<number, string>();\nfunction staircase(n: number) {\n  let s = stairs.get(n);\n  if (!s) {\n    // ease-out cubic: travel p at time t = 1 - (1 - p)^(1/3); numeral k shows once the travel passes the middle of k\n    const at = (k: number) => `${((1 - Math.cbrt(1 - (k - 0.5) / n)) * 100).toFixed(2)}%`;\n    const points = [\"0 0%\"];\n    for (let k = 1; k <= n; k++) points.push(`${(k - 1) / n} ${at(k)}`, `${k / n} ${at(k)}`);\n    points.push(\"1 100%\");\n    s = `linear(${points.join(\", \")})`;\n    stairs.set(n, s);\n  }\n  return s;\n}\n\n/** A caller's ms, or the token default, on the system's clock. */\nconst clock = (ms: number | undefined, token: string) =>\n  ms === undefined ? token : `calc(${ms}ms * var(--pui-motion-scale, 1) * var(--pui-motion-look, 1))`;\n\n/**\n * Text and numbers that roll into their new value on reels, digit by digit from the right. A number that goes up rolls\n * every reel up and one that goes down rolls them down (9 → 0 on the way up passes through the wrap, like an odometer),\n * so the direction of the change reads before the digits do. A reel ticks through whole numerals, slowing into place, so\n * any frame taken mid-roll reads clean digits; a place the new value no longer has goes at once, and a new place or a\n * changed sign appears whole when its turn in the ripple comes. Reduced motion swaps the value at once. The reels are\n * hidden from assistive tech, which reads the value as text.\n */\nexport function SlotText({\n  value,\n  format,\n  duration,\n  stagger,\n  spins = 0,\n  align: alignProp,\n  announce = false,\n  dir,\n  className,\n  classNames,\n  ref,\n  style,\n}: SlotTextProps) {\n  const locale = useLocale();\n  const scopeDefaults = useComponentDefaults<SlotTextDefaults>(\"slot-text\", [\"align\"]);\n  const align = alignProp ?? scopeDefaults.align ?? \"end\";\n\n  const text =\n    typeof value === \"number\"\n      ? format\n        ? format(value)\n        : numberFormatter(locale, { maximumSignificantDigits: 21 }).format(value)\n      : value;\n  const direction = dir ?? (typeof value === \"number\" || !/\\p{L}/u.test(text) ? \"ltr\" : \"auto\");\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = tokens.reduced || managedReduced;\n  // the change is known in the same render it arrives in (no effect-delayed second pass): what it was, which way a\n  // number went, and a run counter that restarts the character fades\n  const [seen, setSeen] = useState({ text, value });\n  const [from, setFrom] = useState(text);\n  const [trend, setTrend] = useState(0);\n  const [run, setRun] = useState(0);\n  if (text !== seen.text) {\n    setSeen({ text, value });\n    setFrom(seen.text);\n    setTrend(typeof value === \"number\" && typeof seen.value === \"number\" ? Math.sign(value - seen.value) : 0);\n    setRun((r) => r + 1);\n  }\n  // when the roll has landed, the columns the old value needed and the new one does not are let go\n  const settle = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  useEffect(() => {\n    if (from === text) return;\n    const s = tokens.duration.considered * 2 + tokens.stagger.word * Math.max(text.length, from.length);\n    const ms = Math.max((duration ?? s * 1000) + (stagger ?? 0) * Math.max(text.length, from.length), 0) + 50;\n    settle.current = setTimeout(() => setFrom(text), reduced ? 0 : ms);\n    return () => clearTimeout(settle.current);\n  }, [from, text, run, duration, stagger, reduced, tokens.duration.considered, tokens.stagger.word]);\n  // a new roll starts from where each reel rests: the resting mark from the last roll is lifted before paint\n  useLayoutEffect(() => {\n    host.current?.querySelectorAll(\"[data-settled]\").forEach((r) => r.removeAttribute(\"data-settled\"));\n  }, [run]);\n  const len = Math.max(text.length, from.length);\n  const pad = (s: string) => (align === \"end\" ? s.padStart(len, \" \") : s.padEnd(len, \" \"));\n  const now = pad(text),\n    was = pad(from);\n  // a reel holds the digits 2·spins + 3 times; it rests on the middle run, and a roll that passes the wrap lands on\n  // the run beside it, from which the reel is quietly reset to the middle once it stops\n  const base = 10 * (spins + 1);\n  const reel = Array.from({ length: 10 * (2 * spins + 3) }, (_, k) => DIGITS[k % 10]);\n  const durationCss = clock(duration, \"calc(var(--pui-duration-considered) * 2)\");\n  const staggerCss = clock(stagger, \"var(--pui-stagger-word)\");\n  const landed = (e: AnimationEvent<HTMLSpanElement>) => {\n    const target = e.target as HTMLElement;\n    if (target.dataset.slot === \"slot-text-reel\") target.setAttribute(\"data-settled\", \"\");\n  };\n  return (\n    <span\n      ref={attach}\n      data-slot=\"slot-text\"\n      data-trend={trend > 0 ? \"up\" : trend < 0 ? \"down\" : undefined}\n      dir={direction}\n      className={cn(\"relative inline-flex tabular-nums [unicode-bidi:isolate]\", classNames?.root, className)}\n      style={style}\n    >\n      <span\n        data-slot=\"slot-text-spoken\"\n        className={cn(\"sr-only\", classNames?.spoken)}\n        aria-live={announce ? \"polite\" : undefined}\n      >\n        {text}\n      </span>\n      <span\n        aria-hidden\n        data-slot=\"slot-text-visual\"\n        className={cn(\"inline-flex overflow-hidden\", classNames?.visual)}\n        onAnimationEnd={landed}\n      >\n        {[...now].map((ch, i) => {\n          const order = align === \"end\" ? len - 1 - i : i;\n          // a column keeps its element while it stays the same distance from the aligned side, so it can roll\n          const key = align === \"end\" ? `e${order}` : `s${order}`;\n          const delay = `calc(${order} * ${staggerCss})`;\n          const old = was[i];\n          // a place the new value no longer has (1,284 → 488: the \"1\" and the \",\"): gone at once, so the number takes\n          // its new width with the change itself and no half-faded digit sits over its neighbour while the reels turn\n          const padding = align === \"end\" ? i < len - text.length : i >= text.length;\n          if (padding) return null;\n          if (DIGITS.includes(ch)) {\n            const d = Number(ch);\n            const before = Number(old);\n            const changed = old !== ch && DIGITS.includes(old) && run > 0;\n            const turns = changed ? spins * 10 : 0;\n            // the way the number went decides the way every reel turns; a text value turns each the short way\n            const to =\n              !changed || reduced\n                ? base + d\n                : trend > 0\n                  ? base + d + (d > before ? 0 : 10) + turns\n                  : trend < 0\n                    ? base + d - (d < before ? 0 : 10) - turns\n                    : base + d + turns;\n            const fresh = !DIGITS.includes(old) && run > 0 && !reduced;\n            const start = changed && !reduced ? base + before : to;\n            const steps = Math.abs(to - start);\n            return (\n              <span\n                key={key}\n                data-slot=\"slot-text-digit\"\n                className={cn(\n                  \"relative inline-block h-[1lh] overflow-hidden\",\n                  fresh && styles.arrive,\n                  classNames?.digit,\n                )}\n                style={fresh ? { animationDuration: delay } : undefined}\n              >\n                <span data-slot=\"slot-text-measure\" className={cn(\"invisible\", classNames?.measure)}>\n                  0\n                </span>\n                <span\n                  data-slot=\"slot-text-reel\"\n                  className={cn(styles.reel, steps > 0 && (run % 2 ? styles.rollA : styles.rollB), classNames?.reel)}\n                  style={\n                    {\n                      \"--_from\": start,\n                      \"--_to\": to,\n                      \"--_rest\": base + d,\n                      transform: \"translateY(calc(var(--_to) * -1lh))\",\n                      ...(run > 0 &&\n                        !reduced && {\n                          animationDuration: durationCss,\n                          animationDelay: delay,\n                          animationTimingFunction: staircase(Math.max(steps, 1)),\n                        }),\n                    } as CSSProperties\n                  }\n                >\n                  {reel.map((n, k) => (\n                    <span key={k} data-slot=\"slot-text-numeral\" className={cn(\"h-[1lh]\", classNames?.numeral)}>\n                      {n}\n                    </span>\n                  ))}\n                </span>\n              </span>\n            );\n          }\n          return (\n            <span\n              key={`${key}-${ch}-${run}`}\n              data-slot=\"slot-text-character\"\n              className={cn(\n                \"inline-block whitespace-pre\",\n                !reduced && old !== ch && run > 0 && styles.arrive,\n                classNames?.character,\n              )}\n              style={{ animationDuration: delay }}\n            >\n              {ch}\n            </span>\n          );\n        })}\n      </span>\n    </span>\n  );\n}\n\nexport default SlotText;\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/table/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/table/defaults.ts","content":"export type TableDensity = \"compact\" | \"default\" | \"comfortable\";\nexport interface TableDefaults {\n  /** Shared density setting; omit to inherit the surrounding scope or the component default. */\n  density?: TableDensity;\n}\n"},{"path":"beamline/components/table/messages.ts","type":"registry:file","target":"src/ui/beamline/components/table/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy for this helper only. */\nexport const english = {\n  label: \"Data\",\n  panHint: \"Scroll sideways to compare columns\",\n  search: \"Search {label}\",\n  selected: \"{count} selected\",\n  selectedHidden: \"{count} selected · {hidden} not shown\",\n  clearSelection: \"Clear selection\",\n  selectAllMatching: \"Select all {count} matching rows\",\n  tryAgain: \"Try again\",\n  loading: \"Loading…\",\n  selectAllVisible: \"Select all visible rows\",\n  selectRow: \"Select row {id}\",\n  notMeasured: \"Not measured\",\n  noRows: \"No rows match\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/table/record-table.tsx","type":"registry:file","target":"src/ui/beamline/components/table/record-table.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\nimport type { ListProps } from \"../../lib/list\";\nimport type { TableColumn, TableNativeProps } from \"./table\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"./table\";\nimport { useRecordList } from \"../_shared/record-list\";\nimport { ListPagination, ListEnd } from \"../_shared/list-controls\";\nimport { SearchField } from \"../search-field/search-field\";\nimport { ChipGroup } from \"../chip-group/chip-group\";\nimport { Button } from \"../button/button\";\nimport { Checkbox } from \"../checkbox/checkbox\";\nexport default function RecordTable<T>({\n  data,\n  columns,\n  getRowId,\n  onRowClick,\n  toolbar = true,\n  label: labelProp,\n  list,\n  native,\n}: {\n  data: T[];\n  columns: TableColumn<T>[];\n  getRowId: (row: T) => string;\n  onRowClick?: (row: T) => void;\n  toolbar?: boolean;\n  label?: string;\n  list: ListProps;\n  native: TableNativeProps;\n}) {\n  const i18n = useMessages(\"table\", english);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n  const state = useRecordList(list, data, columns);\n  const selectable = list.selectable ?? true;\n  const ids = state.rows.map(getRowId);\n  const all = ids.length > 0 && ids.every((id) => state.selected.includes(id));\n  const some = ids.some((id) => state.selected.includes(id));\n  const hidden = state.selected.filter((id) => !ids.includes(id));\n  const read = (row: T, col: TableColumn<T>) => (col.value ? col.value(row) : (row as Record<string, unknown>)[col.id]);\n  const steps = [\"xs\", \"sm\", \"md\", \"lg\", \"xl\", \"2xl\", \"3xl\"] as const;\n  const widestFold = steps.filter((step) => columns.some((col) => col.fold === step)).at(-1);\n  return (\n    <div\n      data-slot=\"table-list\"\n      className=\"flex min-w-0 flex-col\"\n      aria-busy={list.loading || list.loadingMore || undefined}\n    >\n      {toolbar && (\n        <div data-slot=\"table-toolbar\" className=\"flex flex-wrap items-center gap-2 p-3\">\n          <SearchField\n            label={i18n(\"search\", { label: label.toLocaleLowerCase(i18n.locale) })}\n            value={state.query}\n            onValueChange={state.setQuery}\n          />\n          {columns\n            .filter((c) => c.filterable)\n            .map((col) => (\n              <ChipGroup\n                key={col.id}\n                label={col.header}\n                multiple\n                value={state.filters[col.id] ?? []}\n                onValueChange={(values) => state.setFilters({ ...state.filters, [col.id]: values })}\n                options={(\n                  col.filterValues ?? [\n                    ...new Set(\n                      data.flatMap((row) => {\n                        const v = read(row, col);\n                        return Array.isArray(v) ? v.map(String) : [String(v)];\n                      }),\n                    ),\n                  ]\n                ).map((value) => ({ value, label: value }))}\n              />\n            ))}\n        </div>\n      )}\n      {state.selected.length > 0 && (\n        <div className=\"flex flex-wrap items-center gap-2 px-3 pb-2\" role=\"status\">\n          <span className=\"text-xs text-ink-2\">\n            {i18n(hidden.length ? \"selectedHidden\" : \"selected\", {\n              count: state.selected.length,\n              hidden: hidden.length,\n            })}\n          </span>\n          <Button size=\"sm\" variant=\"ghost\" onClick={() => state.setSelected([])}>\n            {i18n(\"clearSelection\")}\n          </Button>\n          {list.manual &&\n            list.allowSelectAllMatching &&\n            list.onSelectAllMatching &&\n            state.rowCount != null &&\n            state.rowCount > ids.length &&\n            all && (\n              <Button\n                size=\"sm\"\n                variant=\"ghost\"\n                onClick={() =>\n                  list.onSelectAllMatching?.({ rowCount: state.rowCount!, query: state.query, filters: state.filters })\n                }\n              >\n                {i18n(\"selectAllMatching\", { count: state.rowCount })}\n              </Button>\n            )}\n        </div>\n      )}\n      {state.error && (\n        <div role=\"status\" className=\"flex items-center gap-2 p-3 text-sm text-red\">\n          {state.error}\n          {list.onRetry && (\n            <Button size=\"sm\" variant=\"secondary\" onClick={() => void list.onRetry?.()}>\n              {i18n(\"tryAgain\")}\n            </Button>\n          )}\n        </div>\n      )}\n      {list.loading && (\n        <span role=\"status\" className=\"px-3 text-xs text-ink-2\">\n          {i18n(\"loading\")}\n        </span>\n      )}\n      <Table\n        {...native}\n        layout={native.layout ?? (widestFold ? \"records\" : \"table\")}\n        foldAt={native.foldAt ?? widestFold}\n        aria-label={native[\"aria-label\"] ?? label}\n        aria-busy={list.loading || list.loadingMore || undefined}\n      >\n        <TableHeader>\n          <TableRow>\n            {selectable && (\n              <TableHead>\n                <Checkbox\n                  aria-label={i18n(\"selectAllVisible\")}\n                  checked={all ? true : some ? \"indeterminate\" : false}\n                  onCheckedChange={() =>\n                    state.setSelected(\n                      all\n                        ? state.selected.filter((id) => !ids.includes(id))\n                        : [...new Set([...state.selected, ...ids])],\n                    )\n                  }\n                />\n              </TableHead>\n            )}\n            {columns.map((col) => {\n              const sorted = state.sort.find((s) => s.id === col.id);\n              return (\n                <TableHead\n                  key={col.id}\n                  label={col.header}\n                  numeric={col.numeric}\n                  aria-sort={sorted ? (sorted.desc ? \"descending\" : \"ascending\") : undefined}\n                >\n                  {col.sortable === false ? (\n                    col.header\n                  ) : (\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      onClick={(event) => {\n                        const next = sorted\n                          ? sorted.desc\n                            ? null\n                            : { id: col.id, desc: true }\n                          : { id: col.id, desc: false };\n                        state.setSort([\n                          ...(event.shiftKey ? state.sort.filter((s) => s.id !== col.id) : []),\n                          ...(next ? [next] : []),\n                        ]);\n                      }}\n                    >\n                      {col.header}\n                      {sorted ? (sorted.desc ? \" ↓\" : \" ↑\") : \"\"}\n                    </Button>\n                  )}\n                </TableHead>\n              );\n            })}\n          </TableRow>\n        </TableHeader>\n        <TableBody>\n          {state.rows.map((row) => {\n            const id = getRowId(row);\n            return (\n              <TableRow key={id} data-row-id={id} data-state={state.selected.includes(id) ? \"selected\" : undefined}>\n                {selectable && (\n                  <TableCell>\n                    <Checkbox\n                      aria-label={i18n(\"selectRow\", { id })}\n                      checked={state.selected.includes(id)}\n                      onCheckedChange={(checked) =>\n                        state.setSelected(\n                          checked ? [...state.selected, id] : state.selected.filter((key) => key !== id),\n                        )\n                      }\n                    />\n                  </TableCell>\n                )}\n                {columns.map((col, index) => {\n                  const value = read(row, col);\n                  const content = col.cell ? (\n                    col.cell(row, value)\n                  ) : value == null ? (\n                    <span aria-label={i18n(\"notMeasured\")} className=\"unmeasured rounded-chip px-1\">\n                      —\n                    </span>\n                  ) : typeof value === \"number\" ? (\n                    i18n.number(value)\n                  ) : (\n                    String(value)\n                  );\n                  return (\n                    <TableCell\n                      key={col.id}\n                      numeric={col.numeric}\n                      label={col.header}\n                      rowHeader={index === 0}\n                      data-null={value == null || undefined}\n                    >\n                      {index === 0 && onRowClick ? (\n                        <Button size=\"sm\" variant=\"ghost\" onClick={() => onRowClick(row)}>\n                          {content}\n                        </Button>\n                      ) : (\n                        content\n                      )}\n                    </TableCell>\n                  );\n                })}\n              </TableRow>\n            );\n          })}\n          {!state.rows.length && !list.loading && (\n            <TableRow>\n              <TableCell colSpan={columns.length + (selectable ? 1 : 0)}>{list.emptyLabel ?? i18n(\"noRows\")}</TableCell>\n            </TableRow>\n          )}\n        </TableBody>\n      </Table>\n      {state.pagination && (\n        <ListPagination\n          page={state.page}\n          pageSize={state.pageSize}\n          rowCount={state.rowCount}\n          hasMore={list.hasMore}\n          onPageChange={state.setPage}\n          onPageSizeChange={state.setPageSize}\n        />\n      )}\n      <ListEnd {...list} resultKey={JSON.stringify([ids, state.query, state.filters, state.sort])} />\n    </div>\n  );\n}\n"},{"path":"beamline/components/table/table.module.css","type":"registry:file","target":"src/ui/beamline/components/table/table.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  .wrapper {\n    position: relative;\n    width: 100%;\n    min-width: 0;\n    max-width: 100%;\n    overflow-x: auto;\n  }\n  /* A table that declares folds answers to its room (only then: containment would collapse a table sized by its\n     content). A column marked fold=\"md\" leaves when the room is narrower than that step and its values join the name\n     line (TableFold at=\"md\"); as the room grows the line leaves and the column comes back, fading in where it stands.\n     Steps are the shared room steps: xs 20 · sm 24 · md 28 · lg 32 · xl 36 · 2xl 42 · 3xl 48rem. The hiding rules are\n     important so a display utility on a fold line or cell (flex, grid) cannot keep both versions on screen. */\n  .wrapper:has(.fold, [data-fold]),\n  .wrapper[data-layout=\"records\"] {\n    container-type: inline-size;\n  }\n  .fold {\n    display: block;\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    line-height: var(--pui-leading-heading);\n  }\n  .head[data-fold],\n  .cell[data-fold],\n  .fold {\n    transition: opacity var(--pui-motion-in) var(--pui-ease-default);\n  }\n  @starting-style {\n    .head[data-fold],\n    .cell[data-fold],\n    .fold {\n      opacity: 0;\n    }\n  }\n  .root {\n    --_table-row-height: var(--pui-table-row-height, var(--_table-density-height, var(--_pui-table-row-height, 40px)));\n    width: 100%;\n    border-collapse: collapse;\n    caption-side: bottom;\n    font-size: var(--pui-text-sm);\n    font-variant-numeric: tabular-nums;\n  }\n  .root[data-density=\"compact\"] {\n    --_table-density-height: 32px;\n  }\n  .root[data-density=\"default\"] {\n    --_table-density-height: 40px;\n  }\n  .root[data-density=\"comfortable\"] {\n    --_table-density-height: 48px;\n  }\n  .row {\n    border-bottom: 1px var(--pui-border-style, solid) var(--pui-line-soft);\n    transition: background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .row:hover {\n    transition: none;\n    background: var(--pui-hover);\n  }\n  .row[data-state=\"selected\"],\n  .cell[data-state=\"selected\"] {\n    background: var(--pui-accent-tint);\n  }\n  .row[data-state=\"selected\"] > *,\n  .cell[data-state=\"selected\"] {\n    color: var(--pui-ink);\n  }\n  .row[data-state=\"selected\"] > :first-child,\n  .cell[data-state=\"selected\"] {\n    box-shadow: inset 2px 0 0 var(--pui-accent-ink);\n  }\n  .row:dir(rtl)[data-state=\"selected\"] > :first-child,\n  .cell:dir(rtl)[data-state=\"selected\"] {\n    box-shadow: inset -2px 0 0 var(--pui-accent-ink);\n  }\n  .header .row:hover,\n  .footer .row:hover {\n    background: transparent;\n  }\n  /* the seam under the header is the stronger line (fitted surfaces: line under a header, line-soft inside) */\n  .header .row {\n    border-bottom-color: var(--pui-line);\n  }\n  .body .row:last-child {\n    border: 0;\n  }\n  .footer {\n    border-top: 1px var(--pui-border-style, solid) var(--pui-line);\n    font-weight: var(--pui-weight-medium);\n  }\n  .footer .cell,\n  .body .row > .cell:first-child {\n    color: var(--pui-ink);\n  }\n  .head,\n  .cell {\n    height: var(--_table-row-height);\n    padding-inline: var(--pui-space-3);\n    vertical-align: middle;\n  }\n  .head {\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-medium);\n    text-align: start;\n    white-space: nowrap;\n  }\n  .cell {\n    color: var(--pui-ink-2);\n    font-weight: inherit;\n    text-align: start;\n    overflow-wrap: normal;\n    word-break: normal;\n    padding-block: max(0px, calc((var(--_table-row-height) - 24px) / 2));\n  }\n  .head[data-numeric],\n  .cell[data-numeric] {\n    text-align: end;\n  }\n  .cell[data-numeric] {\n    font-family: var(--pui-font-numeric);\n    color: var(--pui-ink);\n  }\n  /* a value that was never measured: a quiet dash, never a zero (the caller adds the words \"Not measured\") */\n  .cell[data-null] {\n    color: var(--pui-ink-3);\n  }\n  /* the border of a collapsed table does not travel with a sticky cell, so the header draws its own seam */\n  .root[data-sticky-header] .head {\n    position: sticky;\n    top: 0;\n    z-index: 1;\n    background: var(--pui-surface);\n    box-shadow: inset 0 -1px 0 var(--pui-line);\n  }\n  .head[data-sticky],\n  .cell[data-sticky] {\n    position: sticky;\n    inset-inline-start: 0;\n    z-index: 1;\n    background: var(--pui-surface);\n  }\n  /* a pinned first cell keeps the row's state paint under it */\n  .row:hover > .cell[data-sticky] {\n    background: var(--pui-hover);\n  }\n  .row[data-state=\"selected\"] > .cell[data-sticky] {\n    background: linear-gradient(var(--pui-accent-tint), var(--pui-accent-tint)), var(--pui-surface);\n  }\n  .root[data-sticky-header] .head[data-sticky] {\n    z-index: 2;\n  }\n  .cell[data-interactive] {\n    padding: 0;\n  }\n  .cell[data-interactive] > :is(button, a) {\n    display: flex;\n    width: 100%;\n    min-height: calc(var(--_table-row-height) - 1px);\n    padding: 0 var(--pui-space-3);\n    align-items: center;\n    border-radius: 0;\n    text-align: inherit;\n    justify-content: inherit;\n  }\n  .cell[data-interactive] > :focus-visible {\n    outline-offset: -2px;\n  }\n  .caption {\n    padding: var(--pui-space-2-5) var(--pui-space-3);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    text-align: start;\n  }\n  .cellLabel {\n    display: none;\n  }\n  .panHint {\n    position: sticky;\n    inset-inline-start: 0;\n    width: fit-content;\n    max-width: 100%;\n    margin: 0;\n    padding: var(--pui-space-2) var(--pui-space-3);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n  }\n  .root[data-layout=\"comparison\"] :is(.cell, .head) {\n    min-inline-size: 10rem;\n    max-inline-size: 28rem;\n    white-space: normal;\n    overflow-wrap: normal;\n    word-break: normal;\n  }\n  .root[data-layout=\"comparison\"] :is(.cell, .head):first-child {\n    position: sticky;\n    inset-inline-start: 0;\n    z-index: 1;\n    background: var(--pui-surface);\n    min-inline-size: 8rem;\n    max-inline-size: 14rem;\n  }\n  @container (width < 20rem) {\n    .root[data-layout=\"records\"][data-fold-at=\"xs\"] {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xs\"] :is(.body, .footer) {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xs\"] .row {\n      display: flex;\n      flex-wrap: wrap;\n      align-items: start;\n      padding-block: var(--pui-space-2);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xs\"] .cell {\n      display: block !important;\n      flex: 1 1 100%;\n      min-inline-size: 0;\n      max-inline-size: 100%;\n      height: auto;\n      text-align: start;\n      padding-block: var(--pui-space-1-5);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xs\"] .cellLabel {\n      display: block;\n      margin-block-end: var(--pui-space-1);\n      color: var(--pui-ink-3);\n      font-size: var(--pui-text-xs);\n      font-family: var(--pui-font-body);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xs\"] .header {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xs\"] .head {\n      display: block !important;\n      height: auto;\n      padding-block: var(--pui-space-1);\n      white-space: normal;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xs\"] .header:not(:has(button, input, [role=\"checkbox\"])) {\n      position: absolute;\n      width: 1px;\n      height: 1px;\n      padding: 0;\n      margin: -1px;\n      overflow: hidden;\n      clip-path: inset(50%);\n      white-space: nowrap;\n    }\n  }\n  @container (width < 24rem) {\n    .root[data-layout=\"records\"][data-fold-at=\"sm\"] {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"sm\"] :is(.body, .footer) {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"sm\"] .row {\n      display: flex;\n      flex-wrap: wrap;\n      align-items: start;\n      padding-block: var(--pui-space-2);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"sm\"] .cell {\n      display: block !important;\n      flex: 1 1 100%;\n      min-inline-size: 0;\n      max-inline-size: 100%;\n      height: auto;\n      text-align: start;\n      padding-block: var(--pui-space-1-5);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"sm\"] .cellLabel {\n      display: block;\n      margin-block-end: var(--pui-space-1);\n      color: var(--pui-ink-3);\n      font-size: var(--pui-text-xs);\n      font-family: var(--pui-font-body);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"sm\"] .header {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"sm\"] .head {\n      display: block !important;\n      height: auto;\n      padding-block: var(--pui-space-1);\n      white-space: normal;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"sm\"] .header:not(:has(button, input, [role=\"checkbox\"])) {\n      position: absolute;\n      width: 1px;\n      height: 1px;\n      padding: 0;\n      margin: -1px;\n      overflow: hidden;\n      clip-path: inset(50%);\n      white-space: nowrap;\n    }\n  }\n  @container (width < 28rem) {\n    .root[data-layout=\"records\"][data-fold-at=\"md\"] {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"md\"] :is(.body, .footer) {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"md\"] .row {\n      display: flex;\n      flex-wrap: wrap;\n      align-items: start;\n      padding-block: var(--pui-space-2);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"md\"] .cell {\n      display: block !important;\n      flex: 1 1 100%;\n      min-inline-size: 0;\n      max-inline-size: 100%;\n      height: auto;\n      text-align: start;\n      padding-block: var(--pui-space-1-5);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"md\"] .cellLabel {\n      display: block;\n      margin-block-end: var(--pui-space-1);\n      color: var(--pui-ink-3);\n      font-size: var(--pui-text-xs);\n      font-family: var(--pui-font-body);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"md\"] .header {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"md\"] .head {\n      display: block !important;\n      height: auto;\n      padding-block: var(--pui-space-1);\n      white-space: normal;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"md\"] .header:not(:has(button, input, [role=\"checkbox\"])) {\n      position: absolute;\n      width: 1px;\n      height: 1px;\n      padding: 0;\n      margin: -1px;\n      overflow: hidden;\n      clip-path: inset(50%);\n      white-space: nowrap;\n    }\n  }\n  @container (width < 32rem) {\n    .root[data-layout=\"records\"][data-fold-at=\"lg\"] {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"lg\"] :is(.body, .footer) {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"lg\"] .row {\n      display: flex;\n      flex-wrap: wrap;\n      align-items: start;\n      padding-block: var(--pui-space-2);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"lg\"] .cell {\n      display: block !important;\n      flex: 1 1 100%;\n      min-inline-size: 0;\n      max-inline-size: 100%;\n      height: auto;\n      text-align: start;\n      padding-block: var(--pui-space-1-5);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"lg\"] .cellLabel {\n      display: block;\n      margin-block-end: var(--pui-space-1);\n      color: var(--pui-ink-3);\n      font-size: var(--pui-text-xs);\n      font-family: var(--pui-font-body);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"lg\"] .header {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"lg\"] .head {\n      display: block !important;\n      height: auto;\n      padding-block: var(--pui-space-1);\n      white-space: normal;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"lg\"] .header:not(:has(button, input, [role=\"checkbox\"])) {\n      position: absolute;\n      width: 1px;\n      height: 1px;\n      padding: 0;\n      margin: -1px;\n      overflow: hidden;\n      clip-path: inset(50%);\n      white-space: nowrap;\n    }\n  }\n  @container (width < 36rem) {\n    .root[data-layout=\"records\"][data-fold-at=\"xl\"] {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xl\"] :is(.body, .footer) {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xl\"] .row {\n      display: flex;\n      flex-wrap: wrap;\n      align-items: start;\n      padding-block: var(--pui-space-2);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xl\"] .cell {\n      display: block !important;\n      flex: 1 1 100%;\n      min-inline-size: 0;\n      max-inline-size: 100%;\n      height: auto;\n      text-align: start;\n      padding-block: var(--pui-space-1-5);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xl\"] .cellLabel {\n      display: block;\n      margin-block-end: var(--pui-space-1);\n      color: var(--pui-ink-3);\n      font-size: var(--pui-text-xs);\n      font-family: var(--pui-font-body);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xl\"] .header {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xl\"] .head {\n      display: block !important;\n      height: auto;\n      padding-block: var(--pui-space-1);\n      white-space: normal;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"xl\"] .header:not(:has(button, input, [role=\"checkbox\"])) {\n      position: absolute;\n      width: 1px;\n      height: 1px;\n      padding: 0;\n      margin: -1px;\n      overflow: hidden;\n      clip-path: inset(50%);\n      white-space: nowrap;\n    }\n  }\n  @container (width < 42rem) {\n    .root[data-layout=\"records\"][data-fold-at=\"2xl\"] {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"2xl\"] :is(.body, .footer) {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"2xl\"] .row {\n      display: flex;\n      flex-wrap: wrap;\n      align-items: start;\n      padding-block: var(--pui-space-2);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"2xl\"] .cell {\n      display: block !important;\n      flex: 1 1 100%;\n      min-inline-size: 0;\n      max-inline-size: 100%;\n      height: auto;\n      text-align: start;\n      padding-block: var(--pui-space-1-5);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"2xl\"] .cellLabel {\n      display: block;\n      margin-block-end: var(--pui-space-1);\n      color: var(--pui-ink-3);\n      font-size: var(--pui-text-xs);\n      font-family: var(--pui-font-body);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"2xl\"] .header {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"2xl\"] .head {\n      display: block !important;\n      height: auto;\n      padding-block: var(--pui-space-1);\n      white-space: normal;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"2xl\"] .header:not(:has(button, input, [role=\"checkbox\"])) {\n      position: absolute;\n      width: 1px;\n      height: 1px;\n      padding: 0;\n      margin: -1px;\n      overflow: hidden;\n      clip-path: inset(50%);\n      white-space: nowrap;\n    }\n  }\n  @container (width < 48rem) {\n    .root[data-layout=\"records\"][data-fold-at=\"3xl\"] {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"3xl\"] :is(.body, .footer) {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"3xl\"] .row {\n      display: flex;\n      flex-wrap: wrap;\n      align-items: start;\n      padding-block: var(--pui-space-2);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"3xl\"] .cell {\n      display: block !important;\n      flex: 1 1 100%;\n      min-inline-size: 0;\n      max-inline-size: 100%;\n      height: auto;\n      text-align: start;\n      padding-block: var(--pui-space-1-5);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"3xl\"] .cellLabel {\n      display: block;\n      margin-block-end: var(--pui-space-1);\n      color: var(--pui-ink-3);\n      font-size: var(--pui-text-xs);\n      font-family: var(--pui-font-body);\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"3xl\"] .header {\n      display: block;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"3xl\"] .head {\n      display: block !important;\n      height: auto;\n      padding-block: var(--pui-space-1);\n      white-space: normal;\n    }\n    .root[data-layout=\"records\"][data-fold-at=\"3xl\"] .header:not(:has(button, input, [role=\"checkbox\"])) {\n      position: absolute;\n      width: 1px;\n      height: 1px;\n      padding: 0;\n      margin: -1px;\n      overflow: hidden;\n      clip-path: inset(50%);\n      white-space: nowrap;\n    }\n  }\n  @container (width < 20rem) {\n    .head[data-fold=\"xs\"],\n    .cell[data-fold=\"xs\"] {\n      display: none !important;\n    }\n  }\n  @container (width >= 20rem) {\n    .fold[data-at=\"xs\"] {\n      display: none !important;\n    }\n  }\n  @container (width < 24rem) {\n    .head[data-fold=\"sm\"],\n    .cell[data-fold=\"sm\"] {\n      display: none !important;\n    }\n  }\n  @container (width >= 24rem) {\n    .fold[data-at=\"sm\"] {\n      display: none !important;\n    }\n  }\n  @container (width < 28rem) {\n    .head[data-fold=\"md\"],\n    .cell[data-fold=\"md\"] {\n      display: none !important;\n    }\n  }\n  @container (width >= 28rem) {\n    .fold[data-at=\"md\"] {\n      display: none !important;\n    }\n  }\n  @container (width < 32rem) {\n    .head[data-fold=\"lg\"],\n    .cell[data-fold=\"lg\"] {\n      display: none !important;\n    }\n  }\n  @container (width >= 32rem) {\n    .fold[data-at=\"lg\"] {\n      display: none !important;\n    }\n  }\n  @container (width < 36rem) {\n    .head[data-fold=\"xl\"],\n    .cell[data-fold=\"xl\"] {\n      display: none !important;\n    }\n  }\n  @container (width >= 36rem) {\n    .fold[data-at=\"xl\"] {\n      display: none !important;\n    }\n  }\n  @container (width < 42rem) {\n    .head[data-fold=\"2xl\"],\n    .cell[data-fold=\"2xl\"] {\n      display: none !important;\n    }\n  }\n  @container (width >= 42rem) {\n    .fold[data-at=\"2xl\"] {\n      display: none !important;\n    }\n  }\n  @container (width < 48rem) {\n    .head[data-fold=\"3xl\"],\n    .cell[data-fold=\"3xl\"] {\n      display: none !important;\n    }\n  }\n  @container (width >= 48rem) {\n    .fold[data-at=\"3xl\"] {\n      display: none !important;\n    }\n  }\n}\n"},{"path":"beamline/components/table/table.tsx","type":"registry:file","target":"src/ui/beamline/components/table/table.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\nimport { lazy, Suspense } from \"react\";\nimport type RecordTableComponent from \"./record-table\";\nconst RecordTable = lazy(() => import(\"./record-table\")) as typeof RecordTableComponent;\nimport type { ReactNode } from \"react\";\nimport type { ListProps } from \"../../lib/list\";\n\nimport { createContext, useContext, useRef, useId, type ComponentProps, type CSSProperties } from \"react\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useFlip } from \"../../hooks/use-flip\";\nimport { cn } from \"../../lib/utils\";\nimport type { TableDefaults, TableDensity } from \"./defaults\";\nimport styles from \"./table.module.css\";\nexport type { TableDefaults, TableDensity } from \"./defaults\";\n\nexport type TableClassNames = Partial<\n  Record<\n    | \"root\"\n    | \"wrap\"\n    | \"header\"\n    | \"body\"\n    | \"footer\"\n    | \"row\"\n    | \"head\"\n    | \"cell\"\n    | \"caption\"\n    | \"fold\"\n    | \"cellLabel\"\n    | \"panHint\",\n    string\n  >\n>;\n/** The shared room steps a column can fold at: xs 20 · sm 24 · md 28 · lg 32 · xl 36 · 2xl 42 · 3xl 48rem. */\nexport type TableFoldStep = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\";\nconst Parts = createContext<TableClassNames>({});\nconst HeadingIds = createContext(\"\");\nconst RowHeadingId = createContext(\"\");\nexport interface TableColumn<T> {\n  id: string;\n  header: string;\n  value?: (row: T) => unknown;\n  cell?: (row: T, value: unknown) => ReactNode;\n  sortable?: boolean;\n  filterable?: boolean;\n  filterValues?: string[];\n  numeric?: boolean;\n  fold?: TableFoldStep;\n}\nexport interface TableProps<T = Record<string, unknown>> extends ComponentProps<\"table\">, ListProps {\n  /** Optional record mode; omit to keep composing TableHeader/TableBody children. */\n  data?: T[];\n  columns?: TableColumn<T>[];\n  /** Required for record mode: stable IDs, never page indices. */\n  getRowId?: (row: T) => string;\n  onRowClick?: (row: T) => void;\n  /** Record-mode search and column filters. Default true. */\n  toolbar?: boolean;\n  label?: string;\n  /** Small-room presentation; defaults to table. Record columns with fold opt into records automatically. */\n  layout?: \"table\" | \"records\" | \"comparison\";\n  /** Container width below which records stack; defaults to lg (32rem). */\n  foldAt?: TableFoldStep;\n  /** Accessible name of the comparison scroller; defaults to the table aria-label. */\n  scrollLabel?: string;\n  /** Visible instruction for horizontal comparison. */\n  panHint?: string;\n  /** Fixed row height: 32 / 40 / 48 px. Scoped density is the fallback. */\n  density?: TableDensity;\n  /** Explicit geometry overrides scoped tokens and density. */\n  rowHeight?: number;\n  /** Keeps table headings visible while the table scrolls. Defaults to false. */\n  stickyHeader?: boolean;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: TableClassNames;\n}\n/**\n * Native table for small datasets. Sorting, selection state and operations belong to the caller. It answers to its\n * room when its columns say how: a column marked `fold=\"md\"` leaves a room narrower than 28rem and its values join the\n * name line (`<TableFold at=\"md\">`), so a narrow table reads as a list and a wide one separates the same values into\n * columns. With `<TableBody flip>`, rows marked `data-flip={id}` keep their path: a new row grows in, the rest slide, a\n * removed one fades.\n */\nexport type TableNativeProps = Omit<\n  TableProps,\n  keyof ListProps | \"data\" | \"columns\" | \"getRowId\" | \"onRowClick\" | \"toolbar\" | \"label\"\n>;\n\nfunction NativeTable({\n  density: densityProp,\n  rowHeight,\n  stickyHeader,\n  className,\n  classNames = {},\n  style,\n  layout = \"table\",\n  foldAt = \"lg\",\n  scrollLabel,\n  panHint,\n  ...props\n}: TableNativeProps) {\n  const defaults = useComponentDefaults<TableDefaults>(\"table\", [\"density\"]);\n  const explicitDensity = densityProp ?? defaults.density;\n  const height = rowHeight;\n  const i18n = useMessages(\"table\", english);\n  const headingId = useId();\n  return (\n    <Parts.Provider value={classNames}>\n      <HeadingIds.Provider value={headingId}>\n        <div\n          data-slot=\"table-wrap\"\n          data-layout={layout}\n          className={cn(styles.wrapper, classNames.wrap)}\n          tabIndex={layout === \"comparison\" ? 0 : undefined}\n          role={layout === \"comparison\" ? \"region\" : undefined}\n          aria-label={layout === \"comparison\" ? (scrollLabel ?? props[\"aria-label\"] ?? i18n(\"label\")) : undefined}\n          aria-describedby={layout === \"comparison\" ? `${headingId}-pan` : undefined}\n        >\n          {layout === \"comparison\" && (\n            <p id={`${headingId}-pan`} data-slot=\"table-pan-hint\" className={cn(styles.panHint, classNames.panHint)}>\n              {panHint ?? i18n(\"panHint\")} <span aria-hidden>↔</span>\n            </p>\n          )}\n          <table\n            role=\"table\"\n            data-layout={layout}\n            data-fold-at={layout === \"records\" ? foldAt : undefined}\n            data-slot=\"table\"\n            data-density={explicitDensity}\n            data-sticky-header={stickyHeader || undefined}\n            className={cn(styles.root, classNames.root, className)}\n            style={\n              {\n                ...(height === undefined ? {} : { \"--pui-table-row-height\": `${height}px` }),\n                ...style,\n              } as CSSProperties\n            }\n            {...props}\n          />\n        </div>\n      </HeadingIds.Provider>\n    </Parts.Provider>\n  );\n}\n/** Native children remain supported; record mode shares DataGrid's server contract. */\nexport function Table<T = Record<string, unknown>>(props: TableProps<T>) {\n  const {\n    data,\n    columns,\n    getRowId,\n    onRowClick,\n    toolbar,\n    label,\n    manual,\n    page,\n    defaultPage,\n    onPageChange,\n    pageSize,\n    defaultPageSize,\n    onPageSizeChange,\n    pagination,\n    rowCount,\n    total,\n    onEndReached,\n    hasMore,\n    loadingMore,\n    query,\n    defaultQuery,\n    onQueryChange,\n    sort,\n    defaultSort,\n    onSortChange,\n    filters,\n    defaultFilters,\n    onFiltersChange,\n    selected,\n    defaultSelected,\n    onSelectedChange,\n    selectable,\n    loading,\n    error,\n    onRetry,\n    emptyLabel,\n    allowSelectAllMatching,\n    onSelectAllMatching,\n    ...native\n  } = props;\n  if (data === undefined) return <NativeTable {...native} />;\n  if (!getRowId) throw new Error(\"Table record mode requires getRowId for stable selection.\");\n  const list = {\n    manual,\n    page,\n    defaultPage,\n    onPageChange,\n    pageSize,\n    defaultPageSize,\n    onPageSizeChange,\n    pagination,\n    rowCount,\n    total,\n    onEndReached,\n    hasMore,\n    loadingMore,\n    query,\n    defaultQuery,\n    onQueryChange,\n    sort,\n    defaultSort,\n    onSortChange,\n    filters,\n    defaultFilters,\n    onFiltersChange,\n    selected,\n    defaultSelected,\n    onSelectedChange,\n    selectable,\n    loading,\n    error,\n    onRetry,\n    emptyLabel,\n    allowSelectAllMatching,\n    onSelectAllMatching,\n  };\n  return (\n    <Suspense\n      fallback={\n        <NativeTable {...native} aria-busy=\"true\">\n          <TableHeader>\n            <TableRow>\n              {(columns ?? []).map((column) => (\n                <TableHead key={column.id}>{column.header}</TableHead>\n              ))}\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {Array.from({ length: Math.max(1, data.length) }, (_, index) => (\n              <TableRow key={index}>\n                <TableCell colSpan={Math.max(1, columns?.length ?? 1)}>\n                  <span className=\"sheen block h-4 rounded-chip\" />\n                </TableCell>\n              </TableRow>\n            ))}\n          </TableBody>\n        </NativeTable>\n      }\n    >\n      <RecordTable\n        data={data}\n        columns={columns ?? []}\n        getRowId={getRowId}\n        onRowClick={onRowClick}\n        toolbar={toolbar}\n        label={label}\n        list={list}\n        native={native}\n      />\n    </Suspense>\n  );\n}\n\nexport function TableHeader({ className, ...props }: ComponentProps<\"thead\">) {\n  const parts = useContext(Parts);\n  return (\n    <thead role=\"rowgroup\" data-slot=\"table-header\" className={cn(styles.header, parts.header, className)} {...props} />\n  );\n}\nexport function TableBody({\n  flip = false,\n  className,\n  ref,\n  ...props\n}: ComponentProps<\"tbody\"> & {\n  /** Rows marked `data-flip={id}` keep their path: a new row grows in, the rest slide to their places, a removed one\n   *  fades where it stood. Off by default (a cell's own animated content stays its own). */\n  flip?: boolean;\n}) {\n  const parts = useContext(Parts);\n  const body = useRef<HTMLTableSectionElement>(null);\n  const attach = useComposedRef(body, ref);\n  // nothing runs at rest, and nothing at all unless the caller asks\n  useFlip(() => (flip ? body.current : null));\n  return (\n    <tbody\n      role=\"rowgroup\"\n      ref={attach}\n      data-slot=\"table-body\"\n      className={cn(styles.body, parts.body, className)}\n      {...props}\n    />\n  );\n}\nexport function TableFooter({ className, ...props }: ComponentProps<\"tfoot\">) {\n  const parts = useContext(Parts);\n  return (\n    <tfoot role=\"rowgroup\" data-slot=\"table-footer\" className={cn(styles.footer, parts.footer, className)} {...props} />\n  );\n}\n/** A row is a reveal host: a row action marked `.reveal` waits for the row's hover, focus or selection (always shown on\n *  touch). */\nexport function TableRow({ className, ...props }: ComponentProps<\"tr\">) {\n  const parts = useContext(Parts);\n  const id = useId();\n  return (\n    <RowHeadingId.Provider value={id}>\n      <tr role=\"row\" data-slot=\"table-row\" className={cn(styles.row, \"reveal-host\", parts.row, className)} {...props} />\n    </RowHeadingId.Provider>\n  );\n}\nexport interface TableCellProps extends ComponentProps<\"td\"> {\n  /** The real column heading, repeated visually above the value in narrow records. */\n  label?: string;\n  /** Render a native th scope=row without changing the cell presentation. */\n  rowHeader?: boolean;\n  /** Right-aligns tabular numeric content. Defaults to false. */\n  numeric?: boolean;\n  /** Leave the row when the table's room is narrower than this step (mark the column's TableHead the same); show its\n   *  value in the name cell's `<TableFold at>` instead. */\n  fold?: TableFoldStep;\n  /** The caller's native button fills the cell and uses an inset keyboard focus ring. */\n  interactive?: boolean;\n  /** Pin the first column to the logical start edge. */\n  sticky?: boolean;\n}\nexport function TableHead({\n  label,\n  numeric,\n  sticky,\n  fold,\n  className,\n  children,\n  ...props\n}: ComponentProps<\"th\"> & {\n  /** Plain column name when the heading contains a sorting control. */\n  label?: string;\n  /** Right-aligns tabular numeric content. Defaults to false. */\n  numeric?: boolean;\n  /** Keeps this table cell pinned at the inline start. Defaults to false. */\n  sticky?: boolean;\n  /** Room breakpoint below which this column folds; omitted by default to keep the column visible. */\n  fold?: TableFoldStep;\n}) {\n  const parts = useContext(Parts);\n  const headingId = useContext(HeadingIds);\n  return (\n    <th\n      role=\"columnheader\"\n      id={\n        label || typeof children === \"string\"\n          ? `${headingId}-${encodeURIComponent(label ?? String(children))}`\n          : undefined\n      }\n      data-slot=\"table-head\"\n      scope=\"col\"\n      data-numeric={numeric || undefined}\n      data-sticky={sticky || undefined}\n      data-fold={fold}\n      className={cn(styles.head, parts.head, className)}\n      {...props}\n    >\n      {children}\n    </th>\n  );\n}\nexport function TableCell({\n  numeric,\n  interactive,\n  sticky,\n  fold,\n  label,\n  rowHeader,\n  className,\n  children,\n  ...props\n}: TableCellProps) {\n  const parts = useContext(Parts);\n  const headingId = useContext(HeadingIds);\n  const rowHeadingId = useContext(RowHeadingId);\n  const Cell = rowHeader ? \"th\" : \"td\";\n  return (\n    <Cell\n      role={rowHeader ? \"rowheader\" : \"cell\"}\n      id={rowHeader ? rowHeadingId : undefined}\n      scope={rowHeader ? \"row\" : undefined}\n      headers={label ? `${headingId}-${encodeURIComponent(label)}${rowHeader ? \"\" : ` ${rowHeadingId}`}` : undefined}\n      data-slot=\"table-cell\"\n      data-fold={fold}\n      data-numeric={numeric || undefined}\n      data-interactive={interactive || undefined}\n      data-sticky={sticky || undefined}\n      className={cn(styles.cell, parts.cell, className)}\n      {...props}\n    >\n      {label && (\n        <span data-slot=\"table-cell-label\" className={cn(styles.cellLabel, parts.cellLabel)} aria-hidden>\n          {label}\n        </span>\n      )}\n      {children}\n    </Cell>\n  );\n}\n/** The values of folded columns, joined into the name cell: shown under the name only while the room is narrower than\n *  `at` (the step those columns fold at), so a narrow row still says everything and a wide one says it once. */\nexport function TableFold({\n  at,\n  className,\n  ...props\n}: ComponentProps<\"span\"> & {\n  /** Room breakpoint below which the folded cell content appears. Required. */\n  at: TableFoldStep;\n}) {\n  const parts = useContext(Parts);\n  return <span data-slot=\"table-fold\" data-at={at} className={cn(styles.fold, parts.fold, className)} {...props} />;\n}\nexport function TableCaption({ className, ...props }: ComponentProps<\"caption\">) {\n  const parts = useContext(Parts);\n  return <caption data-slot=\"table-caption\" className={cn(styles.caption, parts.caption, className)} {...props} />;\n}\nexport default Table;\n"},{"path":"beamline/components/tag-input/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/tag-input/defaults.ts","content":"/** Pure allowed presentation defaults for TagInput; no component or runtime imports. */\nexport interface TagInputDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/tag-input/messages.ts","type":"registry:file","target":"src/ui/beamline/components/tag-input/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  remove: \"Remove {t}\",\n  of: \"{max} of {max2}\",\n  foldedUntilTheFieldHasFocus: \", {hidden} folded until the field has focus\",\n  backspaceAgainRemoves: \"; Backspace again removes {value}\",\n  limit: \"Up to {count} only.\",\n  duplicate: \"“{value}” is already added.\",\n  count: {\n    one: \"{count} tag\",\n    other: \"{count} tags\",\n  },\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/tag-input/tag-input.module.css","type":"registry:file","target":"src/ui/beamline/components/tag-input/tag-input.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Tag input: chips and the typing area share the frame's lines. Left alone, the tags keep to their lines (useFit) and\n     the typing area steps out of the flow, so the field stays one control high; focus brings the typing area and every\n     tag back. */\n  .frame {\n    height: auto;\n    flex-wrap: wrap;\n    gap: var(--pui-space-1);\n    padding-block: var(--pui-size-1, 0.25rem);\n  }\n  /* one chip tall, so chips and text share one centred row and the frame keeps the field height */\n  .input {\n    min-width: 8ch;\n    min-height: var(--pui-space-6);\n    flex-basis: 8ch;\n  }\n  .input[data-resting] {\n    position: absolute;\n    inset-block: 0;\n    inset-inline-end: 0;\n    width: var(--pui-space-6);\n    min-width: 0;\n    flex-basis: auto;\n    opacity: 0;\n  }\n  /* the fold: how many more, in the chip's own shape, quieter than a tag */\n  /* a duplicate was typed: the tag it repeats lights with the accent and fades back (colour and the words below) */\n  .chip[data-repeat] {\n    animation: repeat var(--pui-duration-flash) var(--pui-ease-default);\n  }\n  @keyframes repeat {\n    from {\n      background-color: var(--pui-accent-tint);\n      box-shadow: inset 0 0 0 1px var(--pui-accent);\n    }\n  }\n}\n"},{"path":"beamline/components/tag-input/tag-input.tsx","type":"registry:file","target":"src/ui/beamline/components/tag-input/tag-input.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 { TagInputDefaults } from \"./defaults\";\nexport type { TagInputDefaults } from \"./defaults\";\n\nimport {\n  useRef,\n  useState,\n  type CSSProperties,\n  type Ref,\n  type ClipboardEvent,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport {\n  Field,\n  FieldCount,\n  FieldFrame,\n  fieldControlClass,\n  useFieldIds,\n  type FieldSize,\n  type FieldClassNames,\n} from \"../_shared/field\";\nimport { FieldChip } from \"../_shared/chip\";\nimport { FoldMore } from \"../_shared/fold-more\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useFit } from \"../../hooks/use-fit\";\nimport { useFlip } from \"../../hooks/use-flip\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport styles from \"./tag-input.module.css\";\n\nexport type TagInputClassNames = Pick<FieldClassNames, \"root\" | \"label\" | \"frame\" | \"description\" | \"error\"> &\n  Partial<Record<\"input\" | \"chip\" | \"chipText\" | \"remove\" | \"status\" | \"more\" | \"count\", string>>;\n\nexport interface TagInputProps {\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  /** Controlled list of entered tags; omit to use defaultValue. */\n  value?: string[];\n  /** Initial list of entered tags when uncontrolled. Defaults to an empty list. */\n  defaultValue?: string[];\n  /** Reports list of entered tags after an edit; no callback by default. */\n  onValueChange?: (value: string[]) => void;\n  /** Text shown before a value is entered or selected. Defaults to no placeholder. */\n  placeholder?: string;\n  /** Supporting text beneath the label or heading. Defaults to no supporting text. */\n  description?: ReactNode;\n  /** Error message shown in place of the usual supporting text. Defaults to no error message. */\n  error?: ReactNode;\n  /** Most tags allowed; the count of them sits at the end of the label's row. */\n  max?: number;\n  /** Say what is wrong with a value (\"Not an email address\"), or nothing to accept it. */\n  validate?: (value: string) => string | undefined;\n  /** Lines the tags take while the field is left alone; the rest fold into \"+N\" and come back when it has focus (or\n   *  as the room grows). Default 1; `false` always shows every tag. */\n  lines?: number | false;\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: FieldSize;\n  /** Prevents interaction with the control. Defaults to false. */\n  disabled?: boolean;\n  /** DOM id used to connect the control and its label. Defaults to an automatically generated control id. */\n  id?: string;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: TagInputClassNames;\n  /** Receives the native input; no ref by default. */\n  ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * Short free-form values as tags: Enter, comma or Tab adds one, pasting a list adds them all, Backspace on an empty\n * field marks the last tag and a second Backspace removes it. A duplicate is not added: the tag it repeats lights up.\n * Left alone, the field keeps to one line and folds the tags that do not fit into \"+N\"; focus shows them all.\n */\nexport function TagInput({\n  label,\n  hideLabel: hideLabelProp,\n  showLabel,\n  value: valueProp,\n  defaultValue = [],\n  onValueChange,\n  placeholder,\n  description,\n  error,\n  max,\n  validate,\n  lines = 1,\n  size: sizeProp,\n  disabled,\n  id,\n  className,\n  style,\n  classNames,\n  ref,\n}: TagInputProps) {\n  const i18n = useMessages(\"tag-input\", englishMessages);\n\n  useDeprecatedProp(\"TagInput\", \"hideLabel\", \"showLabel={false}\", hideLabelProp !== undefined);\n  const hideLabel = showLabel === undefined ? hideLabelProp : !showLabel;\n  const scopeDefaults = useComponentDefaults<TagInputDefaults>(\"tag-input\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const [tags, set] = useControllable(valueProp, defaultValue, onValueChange);\n  const [draft, setDraft] = useState(\"\");\n  const [marked, setMarked] = useState(false);\n  const [note, setNote] = useState<string>();\n  const [focused, setFocused] = useState(false);\n  const input = useRef<HTMLInputElement>(null);\n  const frame = useRef<HTMLDivElement>(null);\n  const controlRef = useComposedRef(input, ref);\n  // chips share the frame with the typing area: they grow in, close up and leave a ghost\n  useFlip(frame);\n  // left alone, the tags keep to their lines and the rest fold into \"+N\" (order kept); focus brings every one back\n  const folding = lines !== false && !focused && tags.length > 0;\n  const fits = useFit(frame, { items: tags.length, lines: lines || 1, enabled: folding }, tags.join(\"\\u0000\"));\n  const shown = folding ? tags.slice(0, fits) : tags;\n  const hidden = tags.length - shown.length;\n  const problem = error ?? note;\n  const { controlId, descriptionId, errorId, aria } = useFieldIds(id, { description, error: problem });\n  const full = max !== undefined && tags.length >= max;\n\n  // the tag a duplicate repeats lights up where it is, so the answer is shown, not only said\n  const light = (tag: string) => {\n    const chip = frame.current?.querySelector<HTMLElement>(`[data-flip=\"${CSS.escape(tag)}\"]`);\n    if (!chip) return;\n    chip.removeAttribute(\"data-repeat\");\n    void chip.offsetWidth; // a second duplicate in a row plays it again\n    chip.setAttribute(\"data-repeat\", \"\");\n  };\n  const add = (raw: string[]) => {\n    const next = [...tags];\n    for (const r of raw.map((s) => s.trim()).filter(Boolean)) {\n      if (max !== undefined && next.length >= max) {\n        setNote(i18n(\"limit\", { count: max }));\n        break;\n      }\n      const same = next.find((t) => t.toLowerCase() === r.toLowerCase());\n      if (same) {\n        setNote(i18n(\"duplicate\", { value: r }));\n        light(same);\n        continue;\n      }\n      const problem = validate?.(r);\n      if (problem) {\n        setNote(problem);\n        continue;\n      }\n      next.push(r);\n    }\n    if (next.length !== tags.length) {\n      set(next);\n      setDraft(\"\");\n    }\n  };\n  const onKey = (e: KeyboardEvent<HTMLInputElement>) => {\n    if ((e.key === \"Enter\" || e.key === \",\" || (e.key === \"Tab\" && draft)) && draft.trim()) {\n      e.preventDefault();\n      setNote(undefined);\n      add([draft]);\n      return;\n    }\n    if (e.key === \"Backspace\" && !draft && tags.length) {\n      e.preventDefault();\n      if (marked) {\n        set(tags.slice(0, -1));\n        setMarked(false);\n      } else setMarked(true);\n      return;\n    }\n    if (e.key === \"Escape\") {\n      setMarked(false);\n      setDraft(\"\");\n    } else if (marked) setMarked(false);\n  };\n  const onPaste = (e: ClipboardEvent<HTMLInputElement>) => {\n    const text = e.clipboardData.getData(\"text\");\n    if (!/[,\\n;]/.test(text)) return;\n    e.preventDefault();\n    setNote(undefined);\n    add(text.split(/[,\\n;]/));\n  };\n  return (\n    <Field\n      label={label}\n      hideLabel={hideLabel}\n      description={description}\n      error={problem}\n      aside={\n        max !== undefined ? (\n          <FieldCount length={tags.length} max={max} near={1} slot=\"tag-input-count\" className={classNames?.count} />\n        ) : undefined\n      }\n      controlId={controlId}\n      descriptionId={descriptionId}\n      errorId={errorId}\n      className={className}\n      style={style}\n      classNames={classNames}\n    >\n      <FieldFrame\n        ref={frame}\n        size={size}\n        classNames={classNames}\n        invalid={!!error}\n        disabled={disabled}\n        className={cn(\n          styles.frame,\n          \"[--pui-nest-inset:calc((var(--_field-height)-1.5rem)/2)] [--pui-nest-radius:var(--pui-field-radius,var(--pui-radius-control))]\",\n        )}\n      >\n        {shown.map((t, i) => (\n          <FieldChip\n            key={t}\n            flip={t}\n            fit\n            className={cn(styles.chip, classNames?.chip)}\n            textClassName={classNames?.chipText}\n            removeClassName={classNames?.remove}\n            removeLabel={i18n(\"remove\", { t: t })}\n            active={marked && i === tags.length - 1}\n            disabled={disabled}\n            onRemove={() => {\n              set(tags.filter((x) => x !== t));\n              input.current?.focus();\n            }}\n          >\n            {t}\n          </FieldChip>\n        ))}\n        {hidden > 0 && (\n          // the fold names what it holds on hover; pointing at it opens the field and every tag comes back\n          <FoldMore\n            names={tags.slice(shown.length)}\n            slot=\"tag-input-more\"\n            onPointerDown={(e) => {\n              e.preventDefault();\n              input.current?.focus();\n            }}\n            className={cn(\"cursor-text\", classNames?.more)}\n          />\n        )}\n        <input\n          ref={controlRef}\n          data-slot=\"tag-input\"\n          data-resting={folding ? \"\" : undefined}\n          {...aria}\n          value={draft}\n          disabled={disabled || full}\n          placeholder={full ? i18n(\"of\", { max: max, max2: max }) : tags.length ? undefined : placeholder}\n          onChange={(e) => {\n            setDraft(e.target.value);\n            setMarked(false);\n            if (note) setNote(undefined);\n          }}\n          onKeyDown={onKey}\n          onPaste={onPaste}\n          onFocus={() => setFocused(true)}\n          onBlur={() => {\n            setFocused(false);\n            setMarked(false);\n            if (draft.trim()) add([draft]);\n          }}\n          // the typing area is one chip tall, so chips and text share one centred row and the frame keeps the field height\n          className={cn(fieldControlClass, styles.input, classNames?.input)}\n        />\n        <span data-slot=\"tag-input-status\" className={cn(\"sr-only\", classNames?.status)} aria-live=\"polite\">\n          {i18n(\"count\", { count: tags.length })}\n          {hidden > 0 ? i18n(\"foldedUntilTheFieldHasFocus\", { hidden: hidden }) : \"\"}\n          {marked ? i18n(\"backspaceAgainRemoves\", { value: tags.at(-1) ?? \"\" }) : \"\"}\n        </span>\n      </FieldFrame>\n    </Field>\n  );\n}\n\nexport default TagInput;\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/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/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/components/usage-meter/messages.ts","type":"registry:file","target":"src/ui/beamline/components/usage-meter/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  freeLabel: \"left\",\n  overLabel: \"over\",\n  of: \"of {value}\",\n  valueText: \"{used} of {limit}, {remaining} {label}\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/usage-meter/usage-meter.module.css","type":"registry:file","target":"src/ui/beamline/components/usage-meter/usage-meter.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A size container (never collapsing when left content-sized): from 28rem the header adds the share of the\n     allowance used, a narrower meter keeps the amounts. */\n  .root {\n    container-type: inline-size;\n    min-width: min(16rem, 100%);\n  }\n  .share {\n    display: none;\n  }\n  @container (width >= 28rem) {\n    .share {\n      display: inline;\n    }\n  }\n  /* The bar is the allowance: a well (inset role, like Progress) that the sources fill in order. Past the limit a\n     second, shorter well holds the overflow in solid red, separated by a notch: the limit is a break in the bar,\n     never a hatch (hatching means \"not measured\"). */\n  .bar {\n    display: flex;\n    gap: var(--pui-space-1);\n    height: 8px;\n  }\n  .track {\n    --m-inset-fill: var(--pui-control-track);\n    position: relative;\n    display: flex;\n    gap: 1px;\n    min-width: 0;\n    overflow: hidden;\n    border-radius: var(--pui-radius-pill);\n  }\n  /* A source's part of the bar, in its series colour. Pointing at it or at its legend entry keeps it and fades the others\n     toward the card (never through transparency); the change arrives at once and eases back over --pui-motion-out. */\n  .segment {\n    position: relative;\n    height: 100%;\n    background: var(--_c);\n    transition:\n      width var(--pui-duration-standard) var(--pui-ease-default),\n      background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .segment[data-dim] {\n    background: color-mix(in oklab, var(--_c) 30%, var(--pui-surface));\n    transition-duration: var(--pui-duration-standard), 0ms;\n  }\n  .item {\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .item[data-focus] {\n    color: var(--pui-ink);\n    transition-duration: 0ms;\n  }\n  .item[data-dim] {\n    color: var(--pui-ink-3);\n    transition-duration: 0ms;\n  }\n  .over {\n    width: 100%;\n    background: var(--pui-red);\n  }\n  .swatch {\n    flex: none;\n    width: var(--pui-space-2);\n    height: var(--pui-space-2);\n    border-radius: calc(var(--pui-radius-chip) / 3);\n    background: var(--_c);\n  }\n  .swatchFree {\n    background: var(--pui-control-track);\n    box-shadow: inset 0 0 0 1px var(--pui-line);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.track {\n  composes: m-inset meter from global;\n}\n.swatchFree {\n  composes: swatch;\n}\n"},{"path":"beamline/components/usage-meter/usage-meter.tsx","type":"registry:file","target":"src/ui/beamline/components/usage-meter/usage-meter.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useState, type CSSProperties, type Ref } from \"react\";\nimport { AlertCircle, AlertTriangle } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { formatNumber } from \"../../lib/number\";\nimport { SlotText } from \"../slot-text/slot-text\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { DeltaGhost, useDeltaGhost } from \"../_shared/delta-ghost\";\nimport styles from \"./usage-meter.module.css\";\n\nexport interface UsageMeterSegment {\n  id: string;\n  label: string;\n  value: number;\n}\n\nexport interface UsageMeterClassNames {\n  root?: string;\n  header?: string;\n  label?: string;\n  value?: string;\n  bar?: string;\n  track?: string;\n  segment?: string;\n  overflow?: string;\n  over?: string;\n  legend?: string;\n  legendItem?: string;\n  swatch?: string;\n  amount?: string;\n  remainder?: string;\n}\n\nexport interface UsageMeterProps {\n  /** What is metered: \"Storage\", \"Seats\". */\n  label: string;\n  /** What fills the allowance, in order. */\n  segments: UsageMeterSegment[];\n  limit: number;\n  /** After each number: \"GB\", \"k\". */\n  unit?: string;\n  decimals?: number;\n  /** Write the numbers with Intl options instead (`{ style: \"currency\", currency: \"USD\" }` gives \"$7.50\"); `unit`, if\n   *  given, still follows. Serializable, so a server component can pass it. */\n  formatOptions?: Intl.NumberFormatOptions;\n  /** The locale the numbers are written in. Defaults to the enclosing UIRoot locale. */\n  locale?: string;\n  /** Words for what is left. Default \"left\". */\n  freeLabel?: string;\n  /** Words for going past the limit. Default \"over\". */\n  overLabel?: string;\n  /** How the sources are told apart: series colours (default), or quiet ink steps, darkest first, for money and other\n   *  amounts where a series colour (red in Pop) would read as an alarm; the overflow keeps its tone either way. */\n  palette?: \"series\" | \"quiet\";\n  /** Share of the limit (0–1) where it turns to a warning. Default 0.9. */\n  warnAt?: number;\n  className?: string;\n  style?: CSSProperties;\n  classNames?: UsageMeterClassNames;\n  ref?: Ref<HTMLDivElement>;\n}\n\n/** An allowance and what fills it: one bar split by source, the remaining room, a warning near the limit and the\n *  overflow in its own red well past a notch at the limit; a change to the total leaves its ghost for a moment. Colour is never the only signal: every number is written in the legend. */\n/** The quiet palette: one ink, darkest first, each step still told apart from the next and from the empty well. */\nconst QUIET = [78, 52, 32, 20];\n\nexport function UsageMeter({\n  label,\n  segments,\n  limit,\n  unit = \"\",\n  decimals = 0,\n  formatOptions,\n  locale,\n  freeLabel: freeLabelProp,\n  overLabel: overLabelProp,\n  warnAt = 0.9,\n  palette = \"series\",\n  className,\n  style,\n  classNames,\n  ref,\n}: UsageMeterProps) {\n  const i18n = useMessages(\"usage-meter\", englishMessages);\n  const freeLabel = freeLabelProp === undefined ? i18n(\"freeLabel\") : freeLabelProp;\n  const overLabel = overLabelProp === undefined ? i18n(\"overLabel\") : overLabelProp;\n\n  const total = segments.reduce((n, s) => n + s.value, 0);\n  const over = total > limit;\n  const fmt = (n: number) =>\n    `${formatNumber(n, { locale: locale ?? i18n.locale, formatOptions: formatOptions ?? { minimumFractionDigits: decimals, maximumFractionDigits: decimals } })}${unit ? (unit.length > 2 ? ` ${unit}` : unit) : \"\"}`;\n  const state = over ? \"over\" : total >= limit * warnAt ? \"warn\" : \"ok\";\n  // the allowance well shows each source up to the limit; whatever lies past it goes to the overflow well\n  let room = limit;\n  const shown = segments.map((s) => {\n    const v = Math.max(0, Math.min(s.value, room));\n    room -= v;\n    return v;\n  });\n  // the total's last change stays a moment as a ghost (_shared/delta-ghost): freed room drains, new use arrives lit\n  const [ghost, clearGhost] = useDeltaGhost(Math.min(total, limit) / limit);\n  const swatch = (i: number) =>\n    ({\n      \"--_c\":\n        palette === \"quiet\"\n          ? `color-mix(in oklab, var(--pui-ink) ${QUIET[i % QUIET.length]}%, transparent)`\n          : `var(--pui-series-${(i % 8) + 1})`,\n    }) as CSSProperties;\n  // one source in focus (its legend entry or its part of the bar pointed at): the others fade toward the card\n  const [focus, setFocus] = useState<string | null>(null);\n  const tokens = useMotionTokens();\n  const lit = (id: string) => ({\n    \"data-focus\": focus === id ? \"\" : undefined,\n    \"data-dim\": focus !== null && focus !== id ? \"\" : undefined,\n    onPointerEnter: () => setFocus(id),\n    onPointerLeave: () => setFocus((f) => (f === id ? null : f)),\n  });\n  return (\n    <div\n      ref={ref}\n      style={style}\n      data-slot=\"usage-meter\"\n      data-state={state}\n      className={cn(styles.root, \"grid gap-2\", classNames?.root, className)}\n    >\n      <div\n        data-slot=\"usage-meter-header\"\n        className={cn(\"flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5 text-sm\", classNames?.header)}\n      >\n        <span data-slot=\"usage-meter-label\" className={cn(\"font-medium text-ink\", classNames?.label)}>\n          {label}\n        </span>\n        <span\n          data-slot=\"usage-meter-value\"\n          className={cn(\n            \"inline-flex items-center gap-1 font-numeric whitespace-nowrap tabular-nums\",\n            classNames?.value,\n            state === \"over\" ? \"text-red\" : state === \"warn\" ? \"text-orange\" : \"text-ink-2\",\n          )}\n        >\n          {state === \"over\" && <AlertCircle aria-hidden className=\"size-3.5 self-center\" />}\n          {state === \"warn\" && <AlertTriangle aria-hidden className=\"size-3.5 self-center\" />}\n          <span className=\"[unicode-bidi:isolate]\">\n            <SlotText value={fmt(total)} stagger={0} duration={tokens.duration.standard * 1000} />{\" \"}\n            <span className=\"text-ink-3\">{i18n(\"of\", { value: fmt(limit) })}</span>\n            <span data-slot=\"usage-meter-share\" className={cn(styles.share, \"text-ink-3\")}>\n              {\" · \"}\n              {i18n.number(total / limit, { style: \"percent\", maximumFractionDigits: 0 })}\n            </span>\n          </span>\n        </span>\n      </div>\n      <div\n        role=\"meter\"\n        aria-label={label}\n        aria-valuemin={0}\n        aria-valuemax={limit}\n        aria-valuenow={total}\n        aria-valuetext={i18n(\"valueText\", {\n          used: fmt(total),\n          limit: fmt(limit),\n          remaining: fmt(Math.abs(total - limit)),\n          label: over ? overLabel : freeLabel,\n          count: Math.abs(total - limit),\n        })}\n        data-slot=\"usage-meter-bar\"\n        className={cn(styles.bar, classNames?.bar)}\n      >\n        <span\n          className={cn(styles.track, classNames?.track)}\n          data-slot=\"usage-meter-track\"\n          style={{ flex: `${limit} 1 0px` }}\n        >\n          <DeltaGhost ghost={ghost} onDone={clearGhost} />\n          {segments.map((s, i) => (\n            <span\n              key={s.id}\n              data-slot=\"usage-meter-segment\"\n              className={cn(styles.segment, classNames?.segment)}\n              style={{ ...swatch(i), width: `${(shown[i] / limit) * 100}%` }}\n              {...lit(s.id)}\n            />\n          ))}\n        </span>\n        {over && (\n          <span\n            data-slot=\"usage-meter-overflow\"\n            className={cn(styles.track, classNames?.overflow)}\n            style={{ flex: `${total - limit} 1 0px` }}\n          >\n            <span data-slot=\"usage-meter-over\" className={cn(styles.over, classNames?.over)} />\n          </span>\n        )}\n      </div>\n      <ul\n        data-slot=\"usage-meter-legend\"\n        className={cn(\"flex flex-wrap gap-x-4 gap-y-1 text-xs text-ink-2\", classNames?.legend)}\n      >\n        {segments.map((s, i) => (\n          <li\n            key={s.id}\n            data-slot=\"usage-meter-legend-item\"\n            className={cn(styles.item, \"flex items-center gap-1.5\", classNames?.legendItem)}\n            {...lit(s.id)}\n          >\n            <span\n              aria-hidden\n              className={cn(styles.swatch, classNames?.swatch)}\n              data-slot=\"usage-meter-swatch\"\n              style={swatch(i)}\n            />\n            {s.label}\n            <span\n              data-slot=\"usage-meter-amount\"\n              className={cn(\"font-numeric text-ink-3 tabular-nums\", classNames?.amount)}\n            >\n              {fmt(s.value)}\n            </span>\n          </li>\n        ))}\n        <li\n          data-slot=\"usage-meter-remainder\"\n          className={cn(\n            \"flex items-center gap-1.5\",\n            over ? \"font-medium text-red\" : \"text-ink-3\",\n            classNames?.remainder,\n          )}\n        >\n          {over ? (\n            <>\n              <AlertCircle aria-hidden className=\"size-3\" />\n              {fmt(total - limit)} {overLabel}\n            </>\n          ) : (\n            <>\n              <span aria-hidden className={styles.swatchFree} />\n              {fmt(limit - total)} {freeLabel}\n            </>\n          )}\n        </li>\n      </ul>\n    </div>\n  );\n}\n\nexport default UsageMeter;\n"},{"path":"beamline/hooks/use-active.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-active.ts","content":"\"use client\";\n\nimport { useContext, useEffect, useState, type RefObject } from \"react\";\n\nimport { ActivityContext } from \"../runtime/activity\";\n\n/**\n * Whether an animated element should be doing work: it is (nearly) on screen and the tab is visible. Every\n * continuously animated component gates its loop, shader or CSS animation on this, so an effect scrolled out of view,\n * behind another tab, or in a collapsed panel costs nothing. One IntersectionObserver per element; no polling.\n */\nexport function useActive(\n  ref: RefObject<Element | null>,\n  {\n    margin = \"120px\",\n    key,\n    idle = true,\n  }: {\n    margin?: string;\n    /** Re-attach when this changes (an element that mounts later). */\n    key?: unknown;\n    /** Pause continuous work at the host idle boundary. Finite sequences set false so they finish. */\n    idle?: boolean;\n  } = {},\n) {\n  const active = useContext(ActivityContext);\n  const [inView, setInView] = useState(false);\n  const [visible, setVisible] = useState(\n    () => typeof document === \"undefined\" || document.visibilityState === \"visible\",\n  );\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") {\n      setInView(true);\n      return;\n    }\n    const io = new IntersectionObserver(([e]) => setInView(e.isIntersecting), { rootMargin: margin });\n    io.observe(el);\n    return () => io.disconnect();\n  }, [ref, margin, key]);\n  useEffect(() => {\n    const on = () => setVisible(document.visibilityState === \"visible\");\n    document.addEventListener(\"visibilitychange\", on);\n    return () => document.removeEventListener(\"visibilitychange\", on);\n  }, []);\n  return (!idle || active) && inView && visible;\n}\n"},{"path":"beamline/hooks/use-apple-platform.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-apple-platform.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { isApplePlatform } from \"../lib/platform\";\n\nconst never = () => () => {};\n\n/** isApplePlatform() for rendering: false on the server and in the first hydrated paint, the real answer after. */\nexport function useApplePlatform() {\n  return useSyncExternalStore(never, isApplePlatform, () => false);\n}\n"},{"path":"beamline/hooks/use-composed-ref.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-composed-ref.ts","content":"\"use client\";\n\nimport { useCallback, type Ref, type RefObject } from \"react\";\n\n/** Keep a control's measurement/focus ref and the caller's ref, including React 19 callback cleanup. */\nexport function useComposedRef<T>(internal: RefObject<T | null>, external?: Ref<T>) {\n  return useCallback(\n    (node: T | null) => {\n      internal.current = node;\n      if (typeof external === \"function\") {\n        const cleanup = external(node);\n        if (typeof cleanup === \"function\")\n          return () => {\n            internal.current = null;\n            cleanup();\n          };\n      } else if (external) external.current = node;\n    },\n    [internal, external],\n  );\n}\n"},{"path":"beamline/hooks/use-content-width.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-content-width.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\nconst same = (width: number) => width;\n\n/**\n * The width of an element's content box, kept current with a ResizeObserver. It is first read in a layout effect, so\n * a layout chosen by width (a narrow mode, a chart's scale) is right in the first frame instead of jumping a frame\n * later. `fallback` stands in on the server and until the element exists; `key` re-measures when the element is\n * replaced (one that exists only in one view).\n */\nexport function useContentWidth(ref: RefObject<HTMLElement | null>, fallback: number, key?: unknown): number {\n  return useWidthAs(ref, same, fallback, key);\n}\n\n/**\n * The same width, kept as what a part needs from it (`(w) => w < 640` for a narrow mode): the part renders again only\n * when that changes, not at every pixel.\n */\nexport function useWidthAs<T>(\n  ref: RefObject<HTMLElement | null>,\n  read: (width: number) => T,\n  fallback: T,\n  key?: unknown,\n): T {\n  const [value, setValue] = useState(fallback);\n  const latest = useRef(read);\n  latest.current = read;\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    // the computed width is fractional and ignores transforms, like the observer's content box\n    const s = getComputedStyle(el);\n    const edges = [\"paddingLeft\", \"paddingRight\", \"borderLeftWidth\", \"borderRightWidth\"] as const;\n    const box = parseFloat(s.width);\n    const outer = Number.isFinite(box) ? box : el.offsetWidth;\n    const inner =\n      Number.isFinite(box) && s.boxSizing !== \"border-box\" ? 0 : edges.reduce((w, k) => w + parseFloat(s[k]), 0);\n    setValue(latest.current(outer - inner));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(latest.current(e.contentRect.width)));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key]);\n  return value;\n}\n\n/**\n * The height of an element's content box, kept current like `useContentWidth`: for parts that lay out at whatever\n * height their parent gives them (a chart, a map or a book that fills its panel). Read in a layout effect, so the first\n * frame is right; with `enabled` false nothing is measured and the fallback stands.\n */\nexport function useContentHeight(\n  ref: RefObject<HTMLElement | null>,\n  fallback: number,\n  key?: unknown,\n  enabled = true,\n): number {\n  const [value, setValue] = useState(fallback);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!enabled || !el) return;\n    const s = getComputedStyle(el);\n    setValue(el.clientHeight - (parseFloat(s.paddingTop) || 0) - (parseFloat(s.paddingBottom) || 0));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(e.contentRect.height));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key, enabled]);\n  return enabled ? value : fallback;\n}\n"},{"path":"beamline/hooks/use-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-elapsed.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-elapsed.ts","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/** Whole seconds since `active` became true (or since `since`, a Date.now() time), ticking each second for \"Saving…\n *  4s\"; 0 while inactive. */\nexport function useElapsed(active: boolean, since?: number) {\n  const [seconds, setSeconds] = useState(0);\n  useEffect(() => {\n    if (!active) return;\n    const start = since ?? Date.now();\n    const id = setInterval(() => setSeconds(Math.floor((Date.now() - start) / 1000)), 1000);\n    return () => {\n      clearInterval(id);\n      setSeconds(0);\n    };\n  }, [active, since]);\n  return seconds;\n}\n"},{"path":"beamline/hooks/use-expiring.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-expiring.ts","content":"\"use client\";\n\nimport { useCallback, useEffect, useRef, useState, type SetStateAction } from \"react\";\n\n/**\n * A value that clears itself: a \"Saved\" note, a hint after a short press, a row lit after a jump, an undo offer. Each\n * set shows the value and restarts the countdown (a second nudge keeps it up `ms` from the last one, not the first);\n * after `ms`, or `forMs` for this one set, it is null again. Setting null clears it at once.\n */\nexport function useExpiring<T>(ms: number) {\n  const [value, setValue] = useState<T | null>(null);\n  const timer = useRef<ReturnType<typeof setTimeout>>(undefined);\n  useEffect(() => () => clearTimeout(timer.current), []);\n  const set = useCallback(\n    (next: SetStateAction<T | null>, forMs = ms) => {\n      clearTimeout(timer.current);\n      setValue(next);\n      if (next !== null) timer.current = setTimeout(() => setValue(null), forMs);\n    },\n    [ms],\n  );\n  return [value, set] as const;\n}\n"},{"path":"beamline/hooks/use-fit.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-fit.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\n/**\n * How many of a row's items fit its room: the one fold every element that lists things in a line uses (chips,\n * avatars, breadcrumbs, filters, tags, actions), so a narrow panel shows fewer and a \"+N\" for the rest, and the rest\n * come back as the room grows. Content-sized items keep their natural width; the row decides how many. Items never\n * change order (a fold that reshuffles breaks the reader's memory of where things are): the last ones fold first.\n *\n * Mark each item `data-fit-item` (direct children of the row) and the overflow control (the \"+N\") `data-fit-more`.\n * Every item is measured once at its natural width — all of them render on the first pass and again whenever `items`\n * or `key` changes, read in a layout effect, so nothing is painted half-measured — and then each change of the row's\n * width lays the widths out with the row's own column gap into `lines` lines, keeping room on the last line for the\n * overflow control. Nothing runs at rest: one ResizeObserver on the row.\n *\n * `key` names the item set and must be a stable primitive (a string of the items' ids and labels), never an array or\n * object built during render: a new identity each render would measure again each render, for ever.\n * `keep[i]` marks items that always show (a chosen chip, the current crumb); room for them is taken first.\n * Returns how many of the other items show, in order (all of them while measuring, when off, and when they fit).\n */\nexport function useFit(\n  row: RefObject<HTMLElement | null>,\n  {\n    items,\n    lines = 1,\n    enabled = true,\n    min = 1,\n    keep,\n    from = \"start\",\n  }: {\n    /** How many items there are; a change (or a different `key`) measures them again. */\n    items: number;\n    /** Lines the row may take before it folds. */\n    lines?: number;\n    enabled?: boolean;\n    /** Never show fewer than this many of the other items. */\n    min?: number;\n    keep?: readonly boolean[];\n    /** Which of the other items the room shows: the first that fit (start, the default: the last fold first), or the\n     *  last that fit (end: a breadcrumb folds the levels nearest the top and keeps the parent). The count returned is\n     *  how many show either way; the caller shows them from its end. */\n    from?: \"start\" | \"end\";\n  },\n  key?: string | number,\n): number {\n  const free = keep ? keep.filter((k) => !k).length : items;\n  const [count, setCount] = useState(free);\n  const widths = useRef<number[] | null>(null);\n  const moreWidth = useRef(0);\n  const measuring = useRef(true);\n  const kept = useRef(keep);\n  kept.current = keep;\n  const end = useRef(from === \"end\");\n  end.current = from === \"end\";\n  const recent = useRef<number[]>([]);\n  const last = useRef(free);\n\n  // a new set of items: show them all for one (unpainted) pass so their natural widths can be read\n  useLayoutEffect(() => {\n    widths.current = null;\n    measuring.current = true;\n    setCount(free);\n  }, [items, key, enabled, free]);\n\n  useLayoutEffect(() => {\n    const el = row.current;\n    if (!el || !enabled || typeof ResizeObserver === \"undefined\") return;\n    const layout = () => {\n      if (!widths.current) return;\n      const cs = getComputedStyle(el);\n      const gap = parseFloat(cs.columnGap) || 0;\n      const room = roomOf(el, cs) - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0);\n      // counted from the end, the same placement is read backwards: the last items are the ones that stay\n      const w = end.current ? [...widths.current].reverse() : widths.current;\n      const k = end.current && kept.current ? [...kept.current].reverse() : kept.current;\n      let next = fit(w, room, gap, moreWidth.current, lines, min, k);\n      // a backstop: a row whose room still moves with its own fold (a layout the room reading can't see) settles on\n      // the folded side instead of flipping every frame\n      const now = performance.now();\n      const flips = recent.current.filter((t) => now - t < 500);\n      if (flips.length >= 6) next = Math.min(next, last.current);\n      if (next !== last.current) recent.current = [...flips, now];\n      last.current = next;\n      setCount((c) => (c === next ? c : next));\n    };\n    if (measuring.current) {\n      const found = [...el.querySelectorAll<HTMLElement>(\":scope > [data-fit-item]\")];\n      if (found.length >= items) {\n        widths.current = found.slice(0, items).map((n) => n.getBoundingClientRect().width);\n        const more = el.querySelector<HTMLElement>(\":scope > [data-fit-more]\");\n        // the \"+N\" is not on screen while everything fits: assume a short chip until it has been seen\n        moreWidth.current = more ? more.getBoundingClientRect().width : Math.max(moreWidth.current, 56);\n        measuring.current = false;\n      }\n    }\n    layout();\n    const observer = new ResizeObserver(() => {\n      const more = el.querySelector<HTMLElement>(\":scope > [data-fit-more]\");\n      if (more) moreWidth.current = more.getBoundingClientRect().width;\n      layout();\n    });\n    observer.observe(el);\n    // the room is the parent's line as much as the row's own box: a content-sized row only learns it can grow there\n    if (el.parentElement) observer.observe(el.parentElement);\n    return () => observer.disconnect();\n  });\n\n  return enabled ? count : free;\n}\n\n/**\n * The width a row may take: its own box, or, when it sits content-sized in a flex row (a tab row beside a title in a\n * space-between bar), the line's width less what shares that line with it. Read this way the room never depends on\n * how many items the row shows, so folding can't change the room it was decided by (and flip back).\n */\nfunction roomOf(el: HTMLElement, cs: CSSStyleDeclaration) {\n  const own = el.clientWidth;\n  const parent = el.parentElement;\n  if (!parent) return own;\n  const ps = getComputedStyle(parent);\n  if (!ps.display.includes(\"flex\") || !ps.flexDirection.startsWith(\"row\")) return own;\n  const box = el.getBoundingClientRect();\n  let others = 0;\n  let count = 0;\n  for (const sib of parent.children) {\n    if (sib === el || !(sib instanceof HTMLElement)) continue;\n    const ss = getComputedStyle(sib);\n    if (ss.position === \"absolute\" || ss.position === \"fixed\" || ss.display === \"none\") continue;\n    const r = sib.getBoundingClientRect();\n    // on the row's own line (a wrapped bar puts the others above or below)\n    if (r.bottom <= box.top + 0.5 || r.top >= box.bottom - 0.5) continue;\n    others += r.width + (parseFloat(ss.marginLeft) || 0) + (parseFloat(ss.marginRight) || 0);\n    count++;\n  }\n  const line =\n    parent.clientWidth -\n    (parseFloat(ps.paddingLeft) || 0) -\n    (parseFloat(ps.paddingRight) || 0) -\n    others -\n    count * (parseFloat(ps.columnGap) || 0) -\n    (parseFloat(cs.marginLeft) || 0) -\n    (parseFloat(cs.marginRight) || 0) -\n    // the line is measured outside the row's border; clientWidth is inside it\n    (parseFloat(cs.borderLeftWidth) || 0) -\n    (parseFloat(cs.borderRightWidth) || 0);\n  const cap = cs.maxWidth.endsWith(\"px\") ? parseFloat(cs.maxWidth) : Infinity;\n  return Math.max(own, Math.min(line, cap));\n}\n\n/**\n * How many of the items not kept fit, in order, when the shown ones are laid into `lines` lines of `room` with `gap`\n * between them and `more` kept free on the last line whenever something folds.\n */\nexport function fit(\n  widths: readonly number[],\n  room: number,\n  gap: number,\n  more: number,\n  lines: number,\n  min: number,\n  keep?: readonly boolean[],\n) {\n  const fits = (show: (i: number) => boolean, reserve: number) => {\n    let line = 1;\n    let x = 0;\n    for (let i = 0; i < widths.length; i++) {\n      if (!show(i)) continue;\n      const w = widths[i];\n      const cap = line === lines ? room - reserve : room;\n      const need = x === 0 ? w : x + gap + w;\n      if (need <= cap + 0.5) {\n        x = need;\n        continue;\n      }\n      if (x === 0 || line === lines) return false;\n      line++;\n      if (w > (line === lines ? room - reserve : room) + 0.5) return false;\n      x = w;\n    }\n    return true;\n  };\n  const rank: number[] = [];\n  let free = 0;\n  for (let i = 0; i < widths.length; i++) rank.push(keep?.[i] ? -1 : free++);\n  if (fits(() => true, 0)) return free;\n  // with items that always show, the floor is none of the others (they are what keeps the row from being empty)\n  const floor = keep?.some(Boolean) ? 0 : Math.min(min, free);\n  for (let n = free - 1; n > floor; n--) if (fits((i) => rank[i] < 0 || rank[i] < n, more + gap)) return n;\n  return floor;\n}\n"},{"path":"beamline/hooks/use-flip.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-flip.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, type RefObject } from \"react\";\nimport { useMotionTokens } from \"./use-motion-tokens\";\nimport { cssEase } from \"../lib/motion-tokens\";\n\ntype Seen = { el: HTMLElement; x: number; y: number; w: number; h: number };\n\n/**\n * Items of a collection keep their path (the morph grammar's \"changes keep their size and their path\"): mark each item\n * with `data-flip=\"<stable key>\"` inside `container`. After a change, an item that is new grows in, items that moved\n * slide from where they were, and an item that left fades out as a ghost where it stood (a still copy, inert and\n * hidden from assistive tech, removed when it has faded). The data and focus change at once; only the picture\n * settles. Nothing runs at rest; reduced motion places at once. Positions are kept in the container's own content\n * (relative to it, plus its scroll) and read at rest, so neither a scroll of the page, nor a scroll of the container\n * itself, nor an item still settling reads as movement.\n *\n * `resize`: an item whose box changed size also scales from its old box (for marks without text, a bar or a segment, so a\n * new value glides on transforms instead of laying out every frame). `origin(key)`: an element a new item grows out of\n * (a ribbon's segment becoming a column, a shared chip dividing into pieces); with `originFirst` it applies on the\n * container's first render too, so a panel that opens unfolds its items from what was already on screen. `inner`: items\n * inside another item that lies outside the container (a merge's rows inside a card that moves itself) are this\n * container's own; nesting is judged inside the container only.\n */\nexport function useFlip(\n  container: RefObject<HTMLElement | null> | (() => HTMLElement | null),\n  {\n    ghost = true,\n    enter = true,\n    resize = false,\n    origin,\n    originFirst = false,\n    inner = false,\n  }: {\n    ghost?: boolean;\n    enter?: boolean;\n    resize?: boolean;\n    origin?: (key: string) => Element | null | undefined;\n    originFirst?: boolean;\n    inner?: boolean;\n  } = {},\n) {\n  const get = typeof container === \"function\" ? container : () => container.current;\n  // timing read at the container itself, found when the commit has attached it (a function container has no ref of its\n  // own; without a host the timing stayed unresolved where no UIRoot is around, then rendered again to say so)\n  const host =\n    typeof container === \"function\"\n      ? {\n          get current() {\n            return container();\n          },\n        }\n      : container;\n  const motion = useMotionTokens(host);\n  const last = useRef<Map<string, Seen> | null>(null);\n  const lastWidth = useRef(0);\n  const flying = useRef(new Set<Animation>());\n  // where every item stands, in the container's own content (relative to it, plus its scroll): a list scrolled since the\n  // last render has moved nothing (a row that slid back by the scroll would leave the finger that is tapping it). The\n  // items' own grow-ins and slides still in flight are lifted while they are read, so each box is where the layout puts\n  // it: a render mid-flight (a live price ticking while a new row grows in) is not a move.\n  const read = (root: HTMLElement) => {\n    const lifted = [...flying.current].map((a) => a.effect as KeyframeEffect).map((e) => [e, e.target] as const);\n    for (const [effect] of lifted) effect.target = null;\n    const box = root.getBoundingClientRect();\n    const now = new Map<string, Seen>();\n    for (const el of root.querySelectorAll<HTMLElement>(\"[data-flip]\")) {\n      // only the outermost items move (with `inner`, outermost inside this container)\n      const outer = el.parentElement?.closest(\"[data-flip]\");\n      if (outer && (!inner || (outer !== root && root.contains(outer)))) continue;\n      const r = el.getBoundingClientRect();\n      now.set(el.dataset.flip!, {\n        el,\n        x: r.left - box.left + root.scrollLeft,\n        y: r.top - box.top + root.scrollTop,\n        w: r.width,\n        h: r.height,\n      });\n    }\n    for (const [effect, target] of lifted) effect.target = target;\n    return { box, now };\n  };\n  // a container that changes size at rest, with no render (fonts arriving, a scrollbar appearing, a window resized), has\n  // its places read again then: else the first change after it would take the new width for a reflow and move nothing\n  const watched = useRef<{ el: HTMLElement; observer: ResizeObserver } | null>(null);\n  useLayoutEffect(() => () => watched.current?.observer.disconnect(), []);\n  // every render on purpose: a collection changes through its parent's state, not through anything we can depend on\n  useLayoutEffect(() => {\n    const root = get();\n    // a container that is gone (a panel folded) starts over when it comes back: its items are new, not moved\n    if (!root) {\n      last.current = null;\n      return;\n    }\n    if (watched.current?.el !== root && typeof ResizeObserver !== \"undefined\") {\n      watched.current?.observer.disconnect();\n      const observer = new ResizeObserver(() => {\n        if (flying.current.size || !last.current || !root.isConnected) return;\n        const seen = read(root);\n        last.current = seen.now;\n        lastWidth.current = seen.box.width;\n      });\n      observer.observe(root);\n      watched.current = { el: root, observer };\n    }\n    const { box, now } = read(root);\n    // a container that changed width since the last render reflowed its items by itself (a panel resized, a room\n    // step crossed): that is placement, not a change a person made, so this render records the new places and moves\n    // nothing (a reflow places, only a choice travels)\n    const reflowed = Math.abs(box.width - lastWidth.current) > 0.5;\n    lastWidth.current = box.width;\n    const before = reflowed ? null : last.current;\n    last.current = now;\n    const ms = motion.duration.standard * 1000;\n    // the first render moves nothing, except items that grow out of an origin already on screen\n    if ((!before && !(origin && originFirst)) || motion.reduced || ms < 2) return;\n    const easing = cssEase(motion.ease.standard, \"ease-out\");\n    // an item in flight says so (`data-flip-flight`: \"far\" when it crosses more than its own size, a slide between groups or\n    // a share growing out of another item; \"near\" for a neighbour's step), so a part can lift what travels over other\n    // text onto a surface of its own instead of drawing words over words\n    const fly = (el: HTMLElement, keyframes: Keyframe[], far = false) => {\n      const flight = el.animate(keyframes, { duration: ms, easing });\n      flying.current.add(flight);\n      if (far || el.dataset.flipFlight !== \"far\") el.dataset.flipFlight = far ? \"far\" : \"near\";\n      const land = () => {\n        flying.current.delete(flight);\n        if (![...flying.current].some((a) => (a.effect as KeyframeEffect | null)?.target === el))\n          delete el.dataset.flipFlight;\n      };\n      flight.finished.then(land, land);\n    };\n    const boxOf = (key: string) => {\n      const from = origin?.(key);\n      if (!from || !from.isConnected) return null;\n      const r = from.getBoundingClientRect();\n      return { x: r.left - box.left + root.scrollLeft, y: r.top - box.top + root.scrollTop, w: r.width, h: r.height };\n    };\n    // from a box to where the item stands: translate and scale about the top-left corner\n    const morph = (was: { x: number; y: number; w: number; h: number }, at: Seen): Keyframe[] => [\n      {\n        transformOrigin: \"0 0\",\n        transform: `translate(${was.x - at.x}px, ${was.y - at.y}px) scale(${was.w / Math.max(at.w, 0.5)}, ${was.h / Math.max(at.h, 0.5)})`,\n      },\n      { transformOrigin: \"0 0\", transform: \"none\" },\n    ];\n    for (const [key, at] of now) {\n      const was = before?.get(key);\n      if (!was) {\n        const from = boxOf(key);\n        if (from) fly(at.el, morph(from, at), true);\n        else if (enter && before)\n          fly(at.el, [\n            { opacity: 0, transform: \"scale(0.86)\" },\n            { opacity: 1, transform: \"none\" },\n          ]);\n        continue;\n      }\n      const dx = was.x - at.x,\n        dy = was.y - at.y;\n      const resized = resize && (Math.abs(was.w - at.w) > 0.5 || Math.abs(was.h - at.h) > 0.5);\n      const far = Math.abs(dx) > at.w * 0.5 || Math.abs(dy) > at.h * 1.5;\n      if (resized) fly(at.el, morph(was, at), far);\n      else if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5)\n        fly(at.el, [{ transform: `translate(${dx}px, ${dy}px)` }, { transform: \"none\" }], far);\n    }\n    if (!ghost || !before) return;\n    for (const [key, was] of before) {\n      if (now.has(key) || was.el.isConnected) continue;\n      const copy = was.el.cloneNode(true) as HTMLElement;\n      copy.removeAttribute(\"data-flip\");\n      // the ghost is a picture of the item, not the item: nothing that finds parts by slot or id may find it\n      for (const n of [copy, ...copy.querySelectorAll(\"[id], [data-slot]\")]) {\n        n.removeAttribute(\"id\");\n        n.removeAttribute(\"data-slot\");\n      }\n      copy.setAttribute(\"data-ghost\", \"\");\n      copy.setAttribute(\"aria-hidden\", \"true\");\n      copy.inert = true;\n      if (getComputedStyle(root).position === \"static\") root.style.position = \"relative\";\n      Object.assign(copy.style, {\n        position: \"absolute\",\n        left: `${was.x}px`,\n        top: `${was.y}px`,\n        width: `${was.w}px`,\n        height: `${was.h}px`,\n        margin: \"0\",\n        pointerEvents: \"none\",\n      });\n      root.appendChild(copy);\n      // the ghost is gone by the time a neighbour slides into its place, so their texts never mix. The clock runs\n      // straight and an even curve shapes only the fade: the front-loaded standard curve on the whole timeline passed\n      // 0.45 inside the first frame, so what left read as a pop instead of a fade\n      const fade = copy.animate(\n        [\n          { opacity: 1, transform: \"none\", easing: cssEase(motion.ease.default, \"ease-in-out\") },\n          { opacity: 0, transform: \"scale(0.9)\", offset: 0.6 },\n          { opacity: 0, transform: \"scale(0.86)\" },\n        ],\n        {\n          duration: motion.duration.standard * 1000,\n          easing: \"linear\",\n        },\n      );\n      const drop = () => copy.remove();\n      fade.finished.then(drop, drop);\n      // and by the clock as well: a fade that never reports its end (the page hidden, an animation cancelled from\n      // outside) must not leave a copy behind for a person or a test to find\n      setTimeout(drop, motion.duration.standard * 1000 + 250);\n    }\n  });\n}\n"},{"path":"beamline/hooks/use-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-press-hold.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-press-hold.ts","content":"\"use client\";\n\nimport { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from \"react\";\n\n/** How a hold ended: let go on purpose, or taken away (Escape, the pointer lost, the window or the focus gone). */\nexport type PressEnd = \"release\" | \"cancel\";\n\nexport interface PressHoldOptions {\n  /** The hold began: a press, a finger, Space or Enter on the element (or `start()` from a shortcut). */\n  onStart: () => void;\n  /** The hold ended. A part that commits at the end (push to talk) commits on \"release\"; a part that commits while\n   *  held (hold to confirm) treats both as letting go before it committed. */\n  onEnd?: (how: PressEnd) => void;\n  /** No hold begins, and one in progress is cancelled. */\n  disabled?: boolean;\n}\n\n/**\n * Press and hold on one element, the same way everywhere (hold to confirm, push to talk): a mouse button, a finger or a\n * pen held on it, or Space or Enter while it has focus. The pointer is captured, so a hand that slides off still lets\n * go where it lifts; a long press opens no menu over the hold. Escape cancels a hold before it commits (rule 8), and\n * the element losing focus, the window losing focus or the tab hiding end it, since the release would never arrive.\n * Spread `handlers` on the element; `start` and `end` serve a keyboard shortcut held elsewhere.\n */\nexport function usePressHold({ onStart, onEnd, disabled = false }: PressHoldOptions) {\n  const [pressing, setPressing] = useState(false);\n  const active = useRef(false);\n  const latest = useRef({ onStart, onEnd, disabled });\n  latest.current = { onStart, onEnd, disabled };\n\n  const control = useRef({\n    start() {\n      if (active.current || latest.current.disabled) return;\n      active.current = true;\n      setPressing(true);\n      latest.current.onStart();\n    },\n    end(how: PressEnd) {\n      if (!active.current) return;\n      active.current = false;\n      setPressing(false);\n      latest.current.onEnd?.(how);\n    },\n  }).current;\n\n  // the window or the tab going away ends a hold whose release will never arrive; unmounting ends it too\n  useEffect(() => {\n    const cancel = () => control.end(\"cancel\");\n    const hide = () => {\n      if (document.hidden) cancel();\n    };\n    window.addEventListener(\"blur\", cancel);\n    document.addEventListener(\"visibilitychange\", hide);\n    return () => {\n      window.removeEventListener(\"blur\", cancel);\n      document.removeEventListener(\"visibilitychange\", hide);\n      cancel();\n    };\n  }, [control]);\n  useEffect(() => {\n    if (disabled) control.end(\"cancel\");\n  }, [disabled, control]);\n  // Escape during a hold belongs to the hold: on the window's capture phase it runs before a dialog around the part,\n  // which would otherwise close as well (the same order as useDrag)\n  useEffect(() => {\n    if (!pressing) return;\n    const key = (e: globalThis.KeyboardEvent) => {\n      if (e.key !== \"Escape\") return;\n      e.preventDefault();\n      e.stopImmediatePropagation();\n      control.end(\"cancel\");\n    };\n    window.addEventListener(\"keydown\", key, true);\n    return () => window.removeEventListener(\"keydown\", key, true);\n  }, [pressing, control]);\n\n  // the pointer that holds: a second finger landing on or lifting off the part is not the hand that holds it\n  const holder = useRef<number | null>(null);\n  const mine = (e: PointerEvent<HTMLElement>) => holder.current === e.pointerId;\n  const handlers = {\n    onPointerDown: (e: PointerEvent<HTMLElement>) => {\n      if (e.button !== 0 || !e.isPrimary) return;\n      holder.current = e.pointerId;\n      e.currentTarget.setPointerCapture?.(e.pointerId);\n      control.start();\n    },\n    onPointerUp: (e: PointerEvent<HTMLElement>) => {\n      if (!mine(e)) return;\n      holder.current = null;\n      control.end(\"release\");\n    },\n    onPointerCancel: (e: PointerEvent<HTMLElement>) => {\n      if (!mine(e)) return;\n      holder.current = null;\n      control.end(\"cancel\");\n    },\n    // after a release the capture goes too, and the hold has already ended; before one, the browser took the pointer\n    onLostPointerCapture: (e: PointerEvent<HTMLElement>) => {\n      if (!mine(e)) return;\n      holder.current = null;\n      control.end(\"cancel\");\n    },\n    onKeyDown: (e: KeyboardEvent<HTMLElement>) => {\n      if ((e.key === \" \" || e.key === \"Enter\") && !e.repeat) {\n        e.preventDefault();\n        control.start();\n      }\n    },\n    onKeyUp: (e: KeyboardEvent<HTMLElement>) => {\n      if (e.key === \" \" || e.key === \"Enter\") {\n        e.preventDefault();\n        control.end(\"release\");\n      }\n    },\n    onBlur: () => control.end(\"cancel\"),\n    onContextMenu: (e: { preventDefault(): void }) => e.preventDefault(),\n  };\n\n  return { pressing, handlers, start: control.start, end: control.end };\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/clipboard.ts","type":"registry:file","target":"src/ui/beamline/lib/clipboard.ts","content":"/**\n * Copies text to the clipboard and says whether it worked. Uses the async Clipboard API where the page is allowed to\n * (a secure context with focus), and falls back to a hidden textarea and execCommand elsewhere (http previews,\n * embedded frames).\n */\nexport async function copyText(text: string): Promise<boolean> {\n  try {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(text);\n      return true;\n    }\n  } catch {\n    /* fall through to the textarea */\n  }\n  if (typeof document === \"undefined\") return false;\n  const area = document.createElement(\"textarea\");\n  const previous = document.activeElement as HTMLElement | null;\n  try {\n    area.value = text;\n    area.setAttribute(\"readonly\", \"\");\n    area.style.cssText = \"position:fixed;top:0;left:0;opacity:0;pointer-events:none\";\n    document.body.appendChild(area);\n    area.select();\n    return document.execCommand?.(\"copy\") ?? false;\n  } catch {\n    return false;\n  } finally {\n    area.remove();\n    previous?.focus({ preventScroll: true });\n  }\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/list.ts","type":"registry:file","target":"src/ui/beamline/lib/list.ts","content":"import type { ReactNode } from \"react\";\n\nexport interface ListWindowProps {\n  /** Enable page controls explicitly. Page-related props also enable them, unless onEndReached is supplied. */\n  pagination?: boolean;\n  /** One-based requested page. Default 1. */\n  page?: number;\n  defaultPage?: number;\n  onPageChange?: (page: number) => void;\n  /** Positive rows per page. Default 25. */\n  pageSize?: number;\n  defaultPageSize?: number;\n  onPageSizeChange?: (pageSize: number) => void;\n  /** Total matching rows on the server. null means unknown. Local grids calculate it before slicing. */\n  rowCount?: number | null;\n  /** @deprecated Use rowCount. Canonical rowCount wins, including null. */\n  total?: number | null;\n  /** Append mode: fetch another batch. The app appends it to the supplied rows. */\n  onEndReached?: () => void | Promise<unknown>;\n  /** Whether another batch exists; defaults to true with onEndReached. False stops requests. */\n  hasMore?: boolean;\n  /** An append request is pending; retain every loaded row. */\n  loadingMore?: boolean;\n}\n\nexport type ListSort = { id: string; desc: boolean }[];\nexport type ListFilters = Record<string, string[]>;\n\n/** Shared record-list contract. The app owns requests, cancellation and server mutations. */\nexport interface ListProps extends ListWindowProps {\n  /** Server owns ordering, filters, search and page slicing. Default false. */\n  manual?: boolean;\n  sort?: ListSort;\n  defaultSort?: ListSort;\n  onSortChange?: (sort: ListSort) => void;\n  filters?: ListFilters;\n  defaultFilters?: ListFilters;\n  onFiltersChange?: (filters: ListFilters) => void;\n  query?: string;\n  defaultQuery?: string;\n  onQueryChange?: (query: string) => void;\n  /** Stable row IDs, retained across server pages. */\n  selected?: string[];\n  defaultSelected?: string[];\n  onSelectedChange?: (ids: string[]) => void;\n  selectable?: boolean;\n  loading?: boolean;\n  /** Errors retain the last completed rows. Error objects are rendered by their message. */\n  error?: ReactNode | Error;\n  onRetry?: () => void | Promise<unknown>;\n  emptyLabel?: string;\n  allowSelectAllMatching?: boolean;\n  onSelectAllMatching?: (scope: { rowCount: number; query: string; filters: ListFilters }) => void;\n}\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/number.ts","type":"registry:file","target":"src/ui/beamline/lib/number.ts","content":"import { createFormatters, FALLBACK_LOCALE } from \"../runtime/messages\";\n/** Reads a number typed in `locale` (\"1.234,5\" in de, \"1,234.5\" in en): group separators dropped, the locale's decimal\n *  mark read as the point, currency signs and units ignored. Empty or unreadable text is null; nothing is clamped. */\nexport function parseLocaleNumber(text: string, locale?: string): number | null {\n  if (!text.trim()) return null;\n  // exponent form: \"1e3\" is a thousand and \"2.5e-3\" a fraction (the digits are never run together into 13)\n  const exp = text.trim().match(/^(.*\\d)\\s*e\\s*([+\\u2212-]?\\d+)$/i);\n  if (exp) {\n    const base = parseLocaleNumber(exp[1], locale);\n    return base === null ? null : Number((base * 10 ** Number(exp[2].replace(\"\\u2212\", \"-\"))).toPrecision(15));\n  }\n  const parts = new Intl.NumberFormat(locale).formatToParts(12345.6);\n  const group = parts.find((p) => p.type === \"group\")?.value ?? \",\";\n  const decimal = parts.find((p) => p.type === \"decimal\")?.value ?? \".\";\n  let ascii = text;\n  // one grouping mark that cannot group (not three digits after it) and no decimal mark: the decimal was written the\n  // other locale's way (\"1,5\" in English is one and a half, never fifteen)\n  const marks = text.split(group).length - 1;\n  if (marks === 1 && !text.includes(decimal) && !new RegExp(`\\\\${group}\\\\d{3}(?!\\\\d)`).test(text))\n    ascii = text.replace(group, decimal);\n  if (/[^\\p{ASCII}]/u.test(text)) {\n    const digits = new Intl.NumberFormat(locale, { useGrouping: false });\n    for (let value = 0; value < 10; value++) ascii = ascii.replaceAll(digits.format(value), String(value));\n  }\n  const normalized = ascii\n    .replaceAll(group, \"\")\n    .replaceAll(\"\\u202f\", \"\")\n    .replace(decimal, \".\")\n    .replace(/[\\u2212\\u2013]/g, \"-\")\n    .replace(/[^\\d.-]/g, \"\");\n  if (!/\\d/.test(normalized)) return null;\n  const n = Number(normalized);\n  return Number.isFinite(n) ? n : null;\n}\n\nconst formatters = new Map<string, Intl.NumberFormat>();\n/** A number written from serializable settings, so a server component can pass them where a `format` function cannot\n *  cross: Intl options in a locale, with words before and after (\"$\" … \" / mo\"). One Intl.NumberFormat per setting. */\nexport function formatNumber(\n  value: number,\n  { locale, formatOptions, prefix = \"\", suffix = \"\" }: NumberFormatSettings = {},\n): string {\n  const key = `${locale ?? \"\"}|${JSON.stringify(formatOptions ?? {})}`;\n  let f = formatters.get(key);\n  if (!f) formatters.set(key, (f = new Intl.NumberFormat(locale, formatOptions)));\n  return `${prefix}${f.format(value)}${suffix}`;\n}\n\nexport interface NumberFormatSettings {\n  locale?: string;\n  formatOptions?: Intl.NumberFormatOptions;\n  /** Written before the number: \"$\". */\n  prefix?: string;\n  /** Written after the number: \" ms\", \" / mo\". */\n  suffix?: string;\n}\n\n/** Binary-scaled file sizes; the digits and unit names follow the supplied locale. */\nexport function formatByteSize(bytes: number, locale = FALLBACK_LOCALE, maximumUnit = 3, fractionDigits?: number) {\n  const units = [\"byte\", \"kilobyte\", \"megabyte\", \"gigabyte\"];\n  let value = bytes,\n    unit = 0;\n  while (value >= 1024 && unit < maximumUnit) {\n    value /= 1024;\n    unit++;\n  }\n  const digits = fractionDigits ?? (value < 10 && unit > 0 ? 1 : 0);\n  return createFormatters(locale).number(value, {\n    style: \"unit\",\n    unit: units[unit],\n    unitDisplay: \"short\",\n    minimumFractionDigits: digits,\n    maximumFractionDigits: digits,\n  });\n}\n"},{"path":"beamline/lib/platform.ts","type":"registry:file","target":"src/ui/beamline/lib/platform.ts","content":"/** Whether the person is on an Apple platform, where `mod` is ⌘ (Mac, iPhone, iPad); false where there is no browser. */\nexport const isApplePlatform = () =>\n  typeof navigator !== \"undefined\" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);\n"},{"path":"beamline/lib/utils.ts","type":"registry:file","target":"src/ui/beamline/lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n/* The type roles (utilities.css) and the 2xs step are sizes, not text colours: without this, tailwind-merge reads\n   `text-overline` as a colour and drops it next to `text-ink-3`. A role and a size step still replace each other.\n   Likewise `font-title` is the look's title weight, not a family: read as a family it dropped `font-display` beside it,\n   and every merged page title lost the look's display face. */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-weight\": [{ font: [\"title\"] }],\n      \"font-family\": [{ font: [\"display\", \"numeric\", \"figure\"] }],\n      \"font-size\": [\n        {\n          text: [\n            \"2xs\",\n            \"display\",\n            \"title\",\n            \"heading\",\n            \"subheading\",\n            \"body\",\n            \"body-sm\",\n            \"reading\",\n            \"label\",\n            \"caption\",\n            \"overline\",\n            \"numeric\",\n            \"code\",\n          ],\n        },\n      ],\n    },\n  },\n});\n\n/** Merge class names, letting later Tailwind utilities win over earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/** A small seeded random generator (LCG): the same seed gives the same sequence, for stable sample data and jitter. */\nexport function rng(seed = 1) {\n  let s = seed >>> 0 || 1;\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n"}],"docs":"Settings is in src/ui/beamline/blocks/settings/settings.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/settings"}}