{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"signup-form","type":"registry:block","title":"Sign up form","description":"The account creation card: single sign-on, name, work email and a password with strength and Caps Lock hints, terms consent, inline validation, your request's progress and errors, an emailed-code check and a finished state.","categories":["blocks"],"dependencies":["@radix-ui/react-checkbox@1.3.11","@radix-ui/react-direction@1.1.4","@radix-ui/react-label@2.1.15","@radix-ui/react-popover@1.1.23","@radix-ui/react-slot@1.3.3","clsx@^2.1.1","lucide-react@^1.48.0","motion@^13.4.4","react-hook-form@^7.89.0","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/blocks/signup-form/messages.ts","type":"registry:file","target":"src/ui/beamline/blocks/signup-form/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English defaults; instance labels take precedence. */\nexport const english = {\n  title: \"Create an account\",\n  yourAccountIsReadySignedInAs: \"Your account is ready. Signed in as\",\n  enterTheDigitCodeWeSentTo: \"Enter the {codeLength}-digit code we sent to\",\n  itExpiresIn10Minutes: \". It expires in 10 minutes.\",\n  confirmationCode: \"Confirmation code\",\n  checkingCode: \"Checking code\",\n  checkingCode2: \"Checking code…\",\n  checkYourSpamFolderIfItIsn: \"Check your spam folder if it isn’t there.\",\n  didnTGetItYouCanResend: \"Didn’t get it? You can resend in\",\n  didnTGetIt: \"Didn’t get it?\",\n  sending: \"Sending…\",\n  resendCode: \"Resend code\",\n  changeEmail: \"Change email\",\n  fullName: \"Full name\",\n  mayaLindqvist: \"Maya Lindqvist\",\n  workEmail: \"Work email\",\n  nameCompanyCom: \"name@company.com\",\n  password: \"Password\",\n  atLeastCharactersAShortPhraseWorks: \"At least {minPasswordLength} characters; a short phrase works well.\",\n  iAgreeToThe: \"I agree to the\",\n  and: \"and\",\n  terms: \"Terms\",\n  privacyPolicy: \"Privacy Policy\",\n  createAccount: \"Create account\",\n  haveAnAccount: \"Have an account?\",\n  signIn: \"Sign in\",\n  to: \" to {product}\",\n  confirmYourEmail: \"Confirm your email\",\n  getStartedWithItTakesAboutA: \"Get started with {product}. It takes about a minute.\",\n  itTakesAboutAMinute: \"It takes about a minute.\",\n  authFailure: \"That did not work. Try again.\",\n  providerFailed: \"{provider} sign-up did not finish. Try again or use your email.\",\n  welcome: \"Welcome{value}, {value2}\",\n  enterYourName: \"Enter your name.\",\n  tooEasyToGuessAddLengthOr: \"Too easy to guess. Add length, or mix in numbers and symbols.\",\n  useAtLeastCharacters: \"Use at least {minPasswordLength} characters.\",\n  acceptTheTermsToCreateAnAccount: \"Accept the terms to create an account.\",\n  signUpWith: \"Sign up with\",\n  enterYourWorkEmail: \"Enter your work email.\",\n  chooseAPassword: \"Choose a password.\",\n  fullEmailAddress: \"Enter a full email address, like name@company.com.\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/blocks/signup-form/signup-form.tsx","type":"registry:file","target":"src/ui/beamline/blocks/signup-form/signup-form.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useEffect, useId, useRef, useState, type ReactNode, type Ref, type CSSProperties } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { AlertCircle } from \"lucide-react\";\nimport {\n  AuthDivider,\n  AuthDoneAvatar,\n  AuthProviderList,\n  otpFitClass,\n  useCapsLock,\n  type AuthProviderListClassNames,\n} from \"../../components/_shared/auth\";\nimport { useHeightMorph } from \"../../hooks/use-height-morph\";\nimport { Bezel } from \"../../components/bezel/bezel\";\nimport { Checkbox } from \"../../components/checkbox/checkbox\";\nimport { Form, FormError, FormField, FormSubmit } from \"../../components/form/form\";\nimport { Input } from \"../../components/input/input\";\nimport { OtpInput } from \"../../components/otp-input/otp-input\";\nimport { PasswordField, scorePassword } from \"../../components/password-field/password-field\";\nimport { Spinner } from \"../../components/spinner/spinner\";\nimport { authErrorField, authErrorMessage, isEmailAddress, type AuthAccount, type AuthProvider } from \"../../lib/auth\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport { TextAction } from \"../../components/_shared/text-action\";\n\nexport interface SignupDetails {\n  /** Human-readable name of this record. Required. */\n  name: string;\n  /** Email address shown for this person. Required. */\n  email: string;\n  /** Password provided by the person creating the account. Required. */\n  password: string;\n}\n\nexport type { AuthProviderListClassNames };\nexport type SignupFormClassNames = Partial<\n  Record<\n    | \"avatar\"\n    | \"email\"\n    | \"password\"\n    | \"formError\"\n    | \"divider\"\n    | \"bezel\"\n    | \"bezelInner\"\n    | \"name\"\n    | \"consent\"\n    | \"headingGroup\"\n    | \"heading\"\n    | \"description\"\n    | \"done\"\n    | \"accountEmail\"\n    | \"verify\"\n    | \"address\"\n    | \"otp\"\n    | \"codeStatus\"\n    | \"countdown\"\n    | \"resend\"\n    | \"changeEmail\"\n    | \"start\"\n    | \"alternatives\"\n    | \"providerError\"\n    | \"form\"\n    | \"termsLink\"\n    | \"privacyLink\"\n    | \"consentError\"\n    | \"submit\"\n    | \"root\"\n    | \"footer\"\n    | \"switchMode\"\n    | \"step\",\n    string\n  >\n>;\nexport interface SignupFormProps {\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SignupFormClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the provider component's named parts; no overrides by default. */\n  providerClassNames?: AuthProviderListClassNames;\n  /** Create the account. Reject with AuthFieldError to blame a field (a taken email), or any Error for a form message.\n   *  With onVerifyEmail, resolve once the confirmation code is on its way. */\n  onSubmit: (details: SignupDetails) => Promise<void | AuthAccount>;\n  /** Adds a step that checks the code emailed to the new address. */\n  onVerifyEmail?: (email: string, code: string) => Promise<void | AuthAccount>;\n  /** Offers \"Resend code\" in that step once the countdown ends. */\n  onResendCode?: (email: string) => Promise<void>;\n  /** Single sign-on choices, with your own icons. */\n  providers?: AuthProvider[];\n  /** Start sign-up with a provider (usually a redirect). Resolve with the account if it completes in place. */\n  onProvider?: (provider: AuthProvider) => Promise<void | AuthAccount>;\n  /** Shows \"Have an account? Sign in\". */\n  onSwitchToSignIn?: () => void;\n  /** Runs once the account exists and, when verifying, the email is confirmed. */\n  onSuccess?: (account: AuthAccount) => void;\n  /** With either link, a required consent checkbox names both. */\n  termsHref?: string;\n  /** URL of the privacy policy linked from the form. Defaults to no privacy-policy link. */\n  privacyHref?: string;\n  /** Your product's name, used in the description. */\n  product?: string;\n  /** Default \"Create an account\". */\n  title?: ReactNode;\n  /** Supporting text beneath the label or heading. Defaults to no supporting text. */\n  description?: ReactNode;\n  /** Controlled name, email and password fields; omitted by default to keep edits locally. */\n  values?: SignupDetails;\n  /** Initial field values when uncontrolled; defaults to empty name, email and password strings. */\n  defaultValues?: Partial<SignupDetails>;\n  /** Reports a name, email or password edit; no callback by default, and consent is not included. */\n  onValuesChange?: (values: SignupDetails) => void;\n  /** Default 8. */\n  minPasswordLength?: number;\n  /** Characters in the emailed code. Default 6. */\n  codeLength?: number;\n  /** Seconds before another code can be sent. Default 30. */\n  resendAfter?: number;\n  /** The heading level of each step. Default 1. */\n  headingLevel?: 1 | 2 | 3;\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<HTMLElement>;\n}\n\ntype Step = \"form\" | \"verify\" | \"done\";\ninterface Values extends SignupDetails {\n  terms: boolean;\n}\n\nconst firstName = (name: string) => name.trim().split(/\\s+/)[0] || name;\nconst clock = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, \"0\")}`;\n\n/**\n * The account creation card. Fields say what is wrong as soon as a submit fails and clear as they are fixed; the\n * password shows its strength and whether Caps Lock is on; your onSubmit's promise drives the button, and its\n * rejection lands next to the field it is about. With onVerifyEmail it continues to an emailed-code check.\n */\nexport function SignupForm({\n  onSubmit,\n  onVerifyEmail,\n  onResendCode,\n  providers = [],\n  onProvider,\n  onSwitchToSignIn,\n  onSuccess,\n  termsHref,\n  privacyHref,\n  product,\n  title: titleProp,\n  description,\n  values,\n  defaultValues,\n  onValuesChange,\n  minPasswordLength = 8,\n  codeLength = 6,\n  resendAfter = 30,\n  headingLevel = 1,\n  className,\n  classNames,\n  providerClassNames,\n  style,\n  ref,\n}: SignupFormProps) {\n  const i18n = useMessages(\"signup-form\", englishMessages);\n  const title = titleProp === undefined ? i18n(\"title\") : titleProp;\n\n  const H = `h${headingLevel}` as \"h1\";\n  const headingId = useId();\n  const root = useRef<HTMLElement | null>(null);\n  const composedRef = useComposedRef(root, ref);\n  const reduced = useReducedMotionSafe();\n  const consent = !!(termsHref || privacyHref);\n\n  const [step, setStep] = useState<Step>(\"form\");\n  const [details, setDetails] = useState<SignupDetails | null>(null);\n  const [account, setAccount] = useState<AuthAccount | null>(null);\n  const [busy, setBusy] = useState<string | null>(null);\n  const [otherError, setOtherError] = useState<string | null>(null);\n  const [code, setCode] = useState(\"\");\n  const [codeError, setCodeError] = useState<string | undefined>();\n  const [checking, setChecking] = useState(false);\n  const [resending, setResending] = useState(false);\n  const [wait, setWait] = useState(0);\n  const focusNext = useRef<\"heading\" | \"name\" | \"code\" | null>(null);\n\n  const form = useForm<Values>({\n    defaultValues: { name: \"\", email: \"\", password: \"\", terms: false, ...defaultValues, ...values },\n  });\n  useEffect(() => {\n    if (!values) return;\n    form.setValue(\"name\", values.name);\n    form.setValue(\"email\", values.email);\n    form.setValue(\"password\", values.password);\n  }, [values, form]);\n  const change = (key: keyof SignupDetails, next: string) => {\n    const { name, email, password } = form.getValues();\n    onValuesChange?.({ ...(values ?? { name, email, password }), [key]: next });\n  };\n  const caps = useCapsLock();\n  const submitting = form.formState.isSubmitting;\n  // the card's height follows each step (the form, the emailed code, the welcome)\n  const bezel = useRef<HTMLElement>(null);\n  useHeightMorph(bezel, step);\n\n  useEffect(() => {\n    if (wait <= 0) return;\n    const t = setTimeout(() => setWait((w) => w - 1), 1000);\n    return () => clearTimeout(t);\n  }, [wait]);\n\n  useEffect(() => {\n    const el = root.current,\n      target = focusNext.current;\n    if (!el || !target) return;\n    focusNext.current = null;\n    if (target === \"heading\") el.querySelector<HTMLElement>(\"[data-slot=signup-form-heading]\")?.focus();\n    else if (target === \"name\") form.setFocus(\"name\");\n    else el.querySelector<HTMLElement>(\"[data-slot=otp-input-field]\")?.focus();\n  });\n\n  const go = (next: Step, focus: typeof focusNext.current) => {\n    focusNext.current = focus;\n    setStep(next);\n  };\n  const finish = (acc: AuthAccount) => {\n    setAccount(acc);\n    go(\"done\", \"heading\");\n    onSuccess?.(acc);\n  };\n  const accountOf = (d: SignupDetails, acc: void | AuthAccount) => acc || { name: d.name, email: d.email };\n\n  const submit = async ({ name, email, password }: Values) => {\n    const d = { name: name.trim(), email: email.trim(), password };\n    setOtherError(null);\n    try {\n      const acc = await onSubmit(d);\n      setDetails(d);\n      if (onVerifyEmail) {\n        setCode(\"\");\n        setCodeError(undefined);\n        setWait(resendAfter);\n        go(\"verify\", \"code\");\n      } else finish(accountOf(d, acc));\n    } catch (e) {\n      const field = authErrorField(e);\n      if (field === \"name\" || field === \"email\" || field === \"password\" || field === \"terms\")\n        form.setError(field, { message: authErrorMessage(e, i18n(\"authFailure\")) }, { shouldFocus: true });\n      else throw e;\n    }\n  };\n\n  const verify = async (value: string) => {\n    if (!details || !onVerifyEmail) return;\n    setChecking(true);\n    setCodeError(undefined);\n    try {\n      finish(accountOf(details, await onVerifyEmail(details.email, value)));\n    } catch (e) {\n      setCodeError(authErrorMessage(e, i18n(\"authFailure\")));\n      setCode(\"\");\n      focusNext.current = \"code\";\n    } finally {\n      setChecking(false);\n    }\n  };\n\n  const resend = async () => {\n    if (!details || !onResendCode) return;\n    setResending(true);\n    setCodeError(undefined);\n    try {\n      await onResendCode(details.email);\n      setCode(\"\");\n      setWait(resendAfter);\n      focusNext.current = \"code\";\n    } catch (e) {\n      setCodeError(authErrorMessage(e, i18n(\"authFailure\")));\n    } finally {\n      setResending(false);\n    }\n  };\n\n  const provider = async (p: AuthProvider) => {\n    if (!onProvider) return;\n    setBusy(p.id);\n    setOtherError(null);\n    try {\n      const acc = await onProvider(p);\n      if (acc) finish(acc);\n    } catch (e) {\n      setOtherError(authErrorMessage(e, i18n(\"providerFailed\", { provider: p.label })));\n    } finally {\n      setBusy(null);\n    }\n  };\n\n  const heading = (text: ReactNode, line?: ReactNode) => (\n    <div data-slot=\"signup-form-headingGroup\" className={cn(\"grid gap-1.5\", classNames?.headingGroup)}>\n      <H\n        id={headingId}\n        tabIndex={-1}\n        data-slot=\"signup-form-heading\"\n        className={cn(\"text-heading text-balance text-ink outline-none\", classNames?.heading)}\n      >\n        {text}\n      </H>\n      {line && (\n        <p\n          data-slot=\"signup-form-description\"\n          className={cn(\"text-sm leading-relaxed text-pretty text-ink-2\", classNames?.description)}\n        >\n          {line}\n        </p>\n      )}\n    </div>\n  );\n  let body: ReactNode;\n  if (step === \"done\" && account) {\n    body = (\n      <div\n        className={cn(\"grid justify-items-center gap-4 py-2 text-center\", classNames?.done)}\n        data-slot=\"signup-form-done\"\n      >\n        <AuthDoneAvatar className={classNames?.avatar} name={account.name} src={account.avatar} />\n        {heading(\n          i18n(\"welcome\", { value: product ? i18n(\"to\", { product: product }) : \"\", value2: firstName(account.name) }),\n          <>\n            {i18n(\"yourAccountIsReadySignedInAs\")}{\" \"}\n            <span data-slot=\"signup-form-accountEmail\" className={cn(\"text-ink-2\", classNames?.accountEmail)}>\n              {account.email}\n            </span>\n            .\n          </>,\n        )}\n      </div>\n    );\n  } else if (step === \"verify\" && details) {\n    body = (\n      <div className={cn(\"grid gap-5\", classNames?.verify)} data-slot=\"signup-form-verify\">\n        {heading(\n          i18n(\"confirmYourEmail\"),\n          <>\n            {i18n(\"enterTheDigitCodeWeSentTo\", { codeLength: codeLength })}{\" \"}\n            <span data-slot=\"signup-form-address\" className={cn(\"font-medium text-ink-2\", classNames?.address)}>\n              {details.email}\n            </span>\n            {i18n(\"itExpiresIn10Minutes\")}\n          </>,\n        )}\n        <OtpInput\n          label={i18n(\"confirmationCode\")}\n          showLabel={false}\n          length={codeLength}\n          value={code}\n          onValueChange={(v) => {\n            setCode(v);\n            if (codeError) setCodeError(undefined);\n          }}\n          onComplete={verify}\n          disabled={checking}\n          error={codeError}\n          className={cn(otpFitClass, classNames?.otp)}\n        />\n        <p\n          data-slot=\"signup-form-codeStatus\"\n          aria-live=\"polite\"\n          className={cn(\n            \"flex min-h-5 flex-wrap items-center justify-center gap-x-1.5 gap-y-1 text-sm text-ink-2\",\n            classNames?.codeStatus,\n          )}\n        >\n          {checking ? (\n            <>\n              <Spinner size=\"sm\" label={i18n(\"checkingCode\")} />\n              {i18n(\"checkingCode2\")}\n            </>\n          ) : !onResendCode ? (\n            <>{i18n(\"checkYourSpamFolderIfItIsn\")}</>\n          ) : wait > 0 ? (\n            <>\n              {i18n(\"didnTGetItYouCanResend\")}{\" \"}\n              <span data-slot=\"signup-form-countdown\" className={cn(\"font-numeric text-ink\", classNames?.countdown)}>\n                {clock(wait)}\n              </span>\n            </>\n          ) : (\n            <>\n              {i18n(\"didnTGetIt\")}{\" \"}\n              <TextAction\n                data-slot=\"signup-form-resend\"\n                className={classNames?.resend}\n                disabled={resending}\n                onClick={resend}\n              >\n                {resending ? i18n(\"sending\") : i18n(\"resendCode\")}\n              </TextAction>\n            </>\n          )}\n        </p>\n        <TextAction\n          data-slot=\"signup-form-changeEmail\"\n          className={cn(\"justify-self-center text-sm text-ink-2\", classNames?.changeEmail)}\n          disabled={checking}\n          onClick={() => {\n            setCode(\"\");\n            setCodeError(undefined);\n            go(\"form\", \"name\");\n          }}\n        >\n          {i18n(\"changeEmail\")}\n        </TextAction>\n      </div>\n    );\n  } else {\n    body = (\n      <div className={cn(\"grid gap-5\", classNames?.start)} data-slot=\"signup-form-start\">\n        {heading(\n          title,\n          description ??\n            (product ? i18n(\"getStartedWithItTakesAboutA\", { product: product }) : i18n(\"itTakesAboutAMinute\")),\n        )}\n        {providers.length > 0 && onProvider && (\n          <div data-slot=\"signup-form-alternatives\" className={cn(\"grid gap-2\", classNames?.alternatives)}>\n            <AuthProviderList\n              classNames={providerClassNames}\n              providers={providers}\n              verb={i18n(\"signUpWith\")}\n              busy={busy}\n              disabled={submitting || !!busy}\n              onSelect={provider}\n            />\n            {otherError && (\n              <p\n                data-slot=\"signup-form-providerError\"\n                role=\"alert\"\n                className={cn(\"text-sm leading-snug text-red\", classNames?.providerError)}\n              >\n                {otherError}\n              </p>\n            )}\n          </div>\n        )}\n        {providers.length > 0 && onProvider && <AuthDivider className={classNames?.divider} />}\n        <Form form={form} onSubmit={submit} className={cn(\"gap-4\", classNames?.form)} aria-labelledby={headingId}>\n          <FormField\n            name=\"name\"\n            rules={{ validate: (v: string) => v.trim().length > 0 || i18n(\"enterYourName\") }}\n            render={({ field, error }) => (\n              <Input\n                className={classNames?.name}\n                label={i18n(\"fullName\")}\n                autoComplete=\"name\"\n                placeholder={i18n(\"mayaLindqvist\")}\n                error={error}\n                {...field}\n                value={values?.name ?? field.value}\n                onChange={(event) => {\n                  const next = event.target.value;\n                  field.onChange(values === undefined ? next : values.name);\n                  change(\"name\", next);\n                }}\n              />\n            )}\n          />\n          <FormField\n            name=\"email\"\n            rules={{\n              required: i18n(\"enterYourWorkEmail\"),\n              validate: (v: string) => isEmailAddress(v) || i18n(\"fullEmailAddress\"),\n            }}\n            render={({ field, error }) => (\n              <Input\n                className={classNames?.email}\n                label={i18n(\"workEmail\")}\n                type=\"email\"\n                inputMode=\"email\"\n                autoComplete=\"email\"\n                autoCapitalize=\"none\"\n                spellCheck={false}\n                placeholder={i18n(\"nameCompanyCom\")}\n                error={error}\n                {...field}\n                value={values?.email ?? field.value}\n                onChange={(event) => {\n                  const next = event.target.value;\n                  field.onChange(values === undefined ? next : values.email);\n                  change(\"email\", next);\n                }}\n              />\n            )}\n          />\n          <FormField\n            name=\"password\"\n            rules={{\n              required: i18n(\"chooseAPassword\"),\n              validate: (v: string) =>\n                v.length >= minPasswordLength\n                  ? scorePassword(v) >= 2 || i18n(\"tooEasyToGuessAddLengthOr\")\n                  : i18n(\"useAtLeastCharacters\", { minPasswordLength: minPasswordLength }),\n            }}\n            render={({ field, error }) => (\n              <PasswordField\n                className={classNames?.password}\n                label={i18n(\"password\")}\n                autoComplete=\"new-password\"\n                showStrength\n                error={error}\n                description={\n                  caps.hint ?? i18n(\"atLeastCharactersAShortPhraseWorks\", { minPasswordLength: minPasswordLength })\n                }\n                {...field}\n                {...caps.handlers}\n                value={values?.password ?? field.value}\n                onChange={(event) => {\n                  const next = event.target.value;\n                  field.onChange(values === undefined ? next : values.password);\n                  change(\"password\", next);\n                }}\n                onBlur={() => {\n                  field.onBlur();\n                  caps.handlers.onBlur();\n                }}\n              />\n            )}\n          />\n          {consent && (\n            <FormField\n              name=\"terms\"\n              rules={{ validate: (v: boolean) => v || i18n(\"acceptTheTermsToCreateAnAccount\") }}\n              render={({ field, error }) => (\n                <Checkbox\n                  className={classNames?.consent}\n                  name={field.name}\n                  ref={field.ref}\n                  checked={field.value}\n                  onCheckedChange={(v) => field.onChange(v === true)}\n                  onBlur={field.onBlur}\n                  invalid={!!error}\n                  label={\n                    <>\n                      {i18n(\"iAgreeToThe\")}{\" \"}\n                      {termsHref ? (\n                        <TextAction asChild className={classNames?.termsLink}>\n                          <a data-slot=\"signup-form-termsLink\" href={termsHref}>\n                            {i18n(\"terms\")}\n                          </a>\n                        </TextAction>\n                      ) : (\n                        i18n(\"terms\")\n                      )}{\" \"}\n                      {i18n(\"and\")}{\" \"}\n                      {privacyHref ? (\n                        <TextAction asChild className={classNames?.privacyLink}>\n                          <a data-slot=\"signup-form-privacyLink\" href={privacyHref}>\n                            {i18n(\"privacyPolicy\")}\n                          </a>\n                        </TextAction>\n                      ) : (\n                        i18n(\"privacyPolicy\")\n                      )}\n                    </>\n                  }\n                  description={\n                    error ? (\n                      <span\n                        data-slot=\"signup-form-consentError\"\n                        className={cn(\"flex items-start gap-1.5 text-red\", classNames?.consentError)}\n                      >\n                        <AlertCircle aria-hidden className=\"mt-px size-3.5 shrink-0\" />\n                        {error}\n                      </span>\n                    ) : undefined\n                  }\n                />\n              )}\n            />\n          )}\n          <FormError className={classNames?.formError} />\n          <FormSubmit className={cn(\"w-full\", classNames?.submit)} disabled={!!busy}>\n            {i18n(\"createAccount\")}\n          </FormSubmit>\n        </Form>\n      </div>\n    );\n  }\n\n  return (\n    <section\n      ref={composedRef}\n      style={style}\n      aria-labelledby={headingId}\n      data-slot=\"signup-form\"\n      data-step={step}\n      className={cn(\"@container w-full max-w-[400px]\", classNames?.root, className)}\n    >\n      <Bezel\n        ref={bezel}\n        className={classNames?.bezel}\n        innerClassName={cn(\"p-6 @max-[23.75rem]:p-5\", classNames?.bezelInner)}\n        footer={\n          onSwitchToSignIn && step === \"form\" ? (\n            <p\n              data-slot=\"signup-form-footer\"\n              className={cn(\"w-full py-1.5 text-center text-sm text-ink-2\", classNames?.footer)}\n            >\n              {i18n(\"haveAnAccount\")}{\" \"}\n              <TextAction\n                data-slot=\"signup-form-switchMode\"\n                className={classNames?.switchMode}\n                onClick={onSwitchToSignIn}\n              >\n                {i18n(\"signIn\")}\n              </TextAction>\n            </p>\n          ) : undefined\n        }\n      >\n        <div data-slot=\"signup-form-step\" key={step} className={cn(!reduced && \"animate-enter-rise\", classNames?.step)}>\n          {body}\n        </div>\n      </Bezel>\n    </section>\n  );\n}\n\nexport default SignupForm;\n"},{"path":"beamline/components/_shared/auth.messages.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/auth.messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English defaults; instance labels take precedence. */\nexport const english = {\n  capsLockIsOn: \"Caps Lock is on\",\n  or: \"or\",\n  failure: \"That did not work. Try again.\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/_shared/auth.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/auth.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./auth.messages\";\nimport { useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport { ArrowBigUpDash } from \"lucide-react\";\nimport { Avatar } from \"../avatar/avatar\";\nimport { Button } from \"../button/button\";\nimport { Marker, MarkerLabel } from \"../marker/marker\";\nimport { Spinner } from \"../spinner/spinner\";\nimport { cn } from \"../../lib/utils\";\nimport type { AuthProvider } from \"../../lib/auth\";\n\n/**\n * Small parts the auth blocks (sign-in, signup-form, auth) share, so the cards behave alike: the single sign-on list,\n * the \"or\" rule between it and the form, and the Caps Lock hint for password fields. The step tray (a card's height following its\n * step) is hooks/use-height-morph.\n */\n\n/** One full-width button per provider, text only unless you pass an icon you are licensed to use. The one in flight\n *  shows progress; the others wait until it settles. */\nexport type AuthProviderListClassNames = Partial<Record<\"root\" | \"button\" | \"icon\", string>>;\nexport function AuthProviderList({\n  classNames,\n  providers,\n  verb,\n  busy,\n  disabled,\n  onSelect,\n}: {\n  classNames?: AuthProviderListClassNames;\n  providers: AuthProvider[];\n  /** The words before the provider's name: \"Continue with\", \"Sign up with\". */\n  verb: string;\n  /** The provider whose request is running. */\n  busy?: string | null;\n  disabled?: boolean;\n  onSelect: (provider: AuthProvider) => void;\n}) {\n  return (\n    <div data-slot=\"auth-providers\" className={cn(\"grid gap-2\", classNames?.root)}>\n      {providers.map((p) => (\n        <Button\n          key={p.id}\n          variant=\"secondary\"\n          className={cn(\"w-full\", classNames?.button)}\n          data-provider={p.id}\n          loading={busy === p.id}\n          disabled={disabled && busy !== p.id}\n          onClick={() => onSelect(p)}\n        >\n          {providers.some((p) => p.icon) && (\n            <span\n              data-slot=\"auth-provider-icon\"\n              aria-hidden\n              className={cn(\"inline-flex size-4 shrink-0 items-center justify-center\", classNames?.icon)}\n            >\n              {p.icon}\n            </span>\n          )}\n          {`${verb} ${p.label}`}\n        </Button>\n      ))}\n    </div>\n  );\n}\n\n/** The person who just got in, with a check that draws itself once on their picture: the result shown where it belongs\n *  (the heading beside it says it in words). */\nexport function AuthDoneAvatar({ name, src, className }: { name: string; src?: string; className?: string }) {\n  return (\n    <span data-slot=\"auth-done-avatar\" className=\"relative inline-flex\">\n      <Avatar className={className} name={name} src={src} size=\"xl\" />\n      <span\n        aria-hidden\n        data-slot=\"auth-done-mark\"\n        className=\"m-surface absolute -end-1 -bottom-1 grid size-6 place-items-center rounded-full\"\n      >\n        <Spinner state=\"success\" size=\"sm\" />\n      </span>\n    </span>\n  );\n}\n\n/** The quiet \"or\" between other ways in and the form. */\nexport function AuthDivider({ children: childrenInput, className }: { children?: ReactNode; className?: string }) {\n  const i18n = useMessages(\"_shared-auth\", englishMessages);\n  const children = childrenInput === undefined ? i18n(\"or\") : childrenInput;\n\n  return (\n    <Marker className={className} variant=\"divider\" data-slot=\"auth-divider\">\n      <MarkerLabel>{children}</MarkerLabel>\n    </Marker>\n  );\n}\n\n/**\n * Whether Caps Lock is on while a password field has focus. Spread `handlers` on the field and show `hint` as its\n * description; the hint is a status, so it is read out when it appears.\n */\nexport function useCapsLock() {\n  const i18n = useMessages(\"_shared-auth\", englishMessages);\n\n  const [on, setOn] = useState(false);\n  const read = (e: KeyboardEvent<HTMLInputElement>) => setOn(!!e.getModifierState?.(\"CapsLock\"));\n  return {\n    on,\n    handlers: { onKeyDown: read, onKeyUp: read, onBlur: () => setOn(false) },\n    hint: on ? (\n      <span role=\"status\" data-slot=\"caps-lock-hint\" className=\"inline-flex items-center gap-1 text-orange\">\n        <ArrowBigUpDash aria-hidden className=\"size-3.5\" />\n        {i18n(\"capsLockIsOn\")}\n      </span>\n    ) : undefined,\n  };\n}\n\n/** For OtpInput's className inside an auth card: the 44px code boxes shrink to fit a narrow card instead of overflowing. */\nexport const otpFitClass =\n  \"justify-items-center [&_[data-slot=otp-input]]:w-full [&_[data-slot=otp-input]]:max-w-[294px] [&_[data-slot=otp-input]>div]:min-w-0 [&_[data-slot=otp-input]>div]:flex-1 [&_[data-slot=otp-input]>div>div]:w-auto [&_[data-slot=otp-input]>div>div]:min-w-0 [&_[data-slot=otp-input]>div>div]:flex-1\";\n"},{"path":"beamline/components/_shared/field.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/field.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A field: label (with an optional aside at the end of its row), the control, then one message line. The gap under\n   the control belongs to the message (--pui-field-gap), so a message that arrives or leaves takes its gap with it. */\n  /* a column, not a grid: a message (or a fold) that leaves pulls its gap away with a negative margin, which a flex\n     column honours and a grid track does not (it would stop at zero and stretch the leaving part back open) */\n  .field {\n    --pui-field-gap: var(--pui-space-1-5);\n    /* positioned: a hidden label or a count's spoken words (sr-only, absolutely placed) stay inside the field */\n    position: relative;\n    display: flex;\n    min-width: 0;\n    flex-direction: column;\n    gap: var(--pui-field-gap);\n  }\n  .labelRow {\n    display: flex;\n    min-width: 0;\n    align-items: baseline;\n    gap: var(--pui-space-2);\n  }\n  .aside {\n    margin-inline-start: auto;\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n  }\n\n  /* The message: one line at a time. A new line grows in to its own height (0fr to 1fr, no measuring) while the one it\n   replaces folds away above it, so the height glides from one to the other; a line that leaves with nothing after it\n   takes the gap with it. Arrival over --pui-motion-in, departure over --pui-motion-out; reduced motion keeps the fades. */\n  .messages {\n    display: grid;\n    margin-top: calc(var(--pui-field-gap, var(--pui-space-1-5)) * -1);\n  }\n  .line {\n    display: grid;\n    grid-template-rows: 1fr;\n    opacity: 1;\n    transition:\n      grid-template-rows var(--pui-motion-in) var(--pui-ease-default),\n      opacity var(--pui-fade-in, var(--pui-motion-in)) var(--pui-ease-default);\n  }\n  @starting-style {\n    .line {\n      grid-template-rows: 0fr;\n      opacity: 0;\n    }\n  }\n  /* the line a field is first drawn with does not grow in */\n  .line[data-initial] {\n    transition: none;\n  }\n  /* a swap is one change of size: the old line folds and the new one grows over the same duration and curve, so their\n     sum (the field's height) moves straight from one to the other, with no swell between; the old words fade first */\n  .line[data-leaving] {\n    grid-template-rows: 0fr;\n    opacity: 0;\n    transition:\n      grid-template-rows var(--pui-motion-out) var(--pui-ease-default),\n      opacity var(--pui-fade-in, var(--pui-motion-in)) var(--pui-ease-default);\n    pointer-events: none;\n  }\n  .messages:has(> .line[data-leaving]) > .line:not([data-leaving], [data-initial]) {\n    transition-duration: var(--pui-motion-out), var(--pui-fade-in, var(--pui-motion-in));\n  }\n  .clip {\n    min-height: 0;\n    overflow: clip;\n    overflow-clip-margin: var(--pui-space-1);\n  }\n  .message {\n    display: flex;\n    align-items: flex-start;\n    gap: var(--pui-space-1-5);\n    padding-top: var(--pui-field-gap, var(--pui-space-1-5));\n    font-size: var(--pui-text-xs);\n    line-height: 1.375;\n  }\n  .description {\n    display: block;\n    color: var(--pui-ink-3);\n  }\n  .error {\n    color: var(--pui-red);\n  }\n  .success {\n    color: var(--pui-green);\n  }\n  .icon {\n    flex-shrink: 0;\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n    margin-top: 1px;\n  }\n\n  /* The count in the label's row: tabular, quiet, shown with attention (.reveal) and in words near the limit. */\n  .count {\n    font-variant-numeric: tabular-nums;\n    white-space: nowrap;\n  }\n  .count[data-state=\"near\"],\n  .count[data-state=\"full\"] {\n    color: var(--pui-orange);\n  }\n  .count[data-state=\"over\"] {\n    color: var(--pui-red);\n  }\n\n  /* The status at the end of the frame: the spinner arrives scaled in from its centre, and closes into the check. */\n  .status {\n    display: inline-grid;\n    flex-shrink: 0;\n    place-items: center;\n    animation: status-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  @keyframes status-in {\n    from {\n      opacity: 0;\n      scale: calc(1 - 0.4 * var(--pui-motion-travel, 1));\n    }\n  }\n\n  /* The one frame every text-like control sits in: input, textarea, password, search, number, the select and date\n   triggers. Its paint (fill, edge, hover, focus) is the field role of the look in force (looks.css); invalid,\n   read-only and disabled are drawn here so they read the same in every look. */\n  .frame {\n    --_field-height: var(--pui-control-height-md);\n    position: relative;\n    display: flex;\n    min-width: 0; /* a grid or flex parent may make it narrower than the input's natural width */\n    align-items: center;\n    gap: var(--pui-space-2);\n    min-height: var(--_field-height);\n    padding-inline: var(--pui-space-2-5);\n    border-radius: var(--pui-field-radius, var(--pui-radius-control));\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    cursor: text;\n    transition:\n      box-shadow var(--pui-motion-out) var(--pui-ease-default),\n      background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .frame:hover:not([data-disabled]) {\n    transition-duration: 0ms;\n  }\n  .frame[data-state=\"open\"] {\n    box-shadow: var(--m-field-edge-focus);\n  }\n  .frame:focus-within {\n    transition: none;\n  }\n  .frame[data-disabled] {\n    opacity: var(--pui-opacity-disabled);\n    cursor: not-allowed;\n  }\n  .frame[data-readonly] {\n    --m-field-bg: transparent;\n  }\n  .sm {\n    --_field-height: var(--pui-control-height-sm);\n    padding-inline: var(--pui-space-2);\n  }\n  .lg {\n    --_field-height: var(--pui-control-height-lg);\n    padding-inline: var(--pui-space-3);\n    font-size: var(--pui-text-base);\n  }\n  .multiline {\n    align-items: stretch;\n    padding-block: var(--pui-space-2);\n  }\n\n  /* The control itself: borderless, it takes the frame's look. Focus shows on the frame, so the control's own ring is off\n   (the frame's ring is the visible focus indicator, which is always present when the control has focus). */\n  .control {\n    flex: 1;\n    min-width: 0;\n    height: 100%;\n    min-height: calc(var(--_field-height) - 2px);\n    border: 0;\n    outline: none;\n    background: transparent;\n    color: inherit;\n    font: inherit;\n    letter-spacing: inherit;\n    padding: 0;\n  }\n  .control:focus-visible {\n    outline: none;\n  }\n  .control::placeholder {\n    color: var(--pui-ink-3);\n    opacity: 1;\n  }\n  .control:disabled {\n    cursor: not-allowed;\n  }\n  textarea.control {\n    min-height: 0;\n    resize: none;\n    line-height: 1.5;\n    field-sizing: content;\n  }\n  .adornment {\n    display: inline-flex;\n    align-items: center;\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n  }\n  .adornment :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.frame {\n  composes: m-field nest-corner from global;\n}\n"},{"path":"beamline/components/_shared/field.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/field.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type CSSProperties,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n  type Ref,\n  type TransitionEvent,\n} from \"react\";\nimport { AlertCircle, CircleCheck } from \"lucide-react\";\nimport { Label } from \"../label/label\";\nimport { Spinner } from \"../spinner/spinner\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./field.module.css\";\n\nexport type FieldSize = \"sm\" | \"md\" | \"lg\";\nexport type FieldClassNames = Partial<\n  Record<\"root\" | \"label\" | \"frame\" | \"leading\" | \"trailing\" | \"description\" | \"error\" | \"success\" | \"aside\", string>\n>;\n\nexport interface FieldOptions {\n  /** Visible label, also the accessible name. */\n  label?: ReactNode;\n  /** Keep the label for assistive tech only (dense rows, toolbars). */\n  hideLabel?: boolean;\n  /** One line of help under the control. */\n  description?: ReactNode;\n  /** The problem and its fix; replaces the description and marks the control invalid. */\n  error?: ReactNode;\n  /** The confirmation that the value works (\"@mira is available\"): replaces the description, with a check. */\n  success?: ReactNode;\n  required?: boolean;\n  size?: FieldSize;\n}\n\n/** Ids and aria wiring for one field: the control gets these spread onto it. */\nexport function useFieldIds(\n  id?: string,\n  { description, error, success }: Pick<FieldOptions, \"description\" | \"error\" | \"success\"> = {},\n) {\n  const auto = useId();\n  const controlId = id ?? `field-${auto}`;\n  // the line under the field is one of three, and the control points at whichever is there\n  const descriptionId = (success || description) && !error ? `${controlId}-description` : undefined;\n  const errorId = error ? `${controlId}-error` : undefined;\n  return {\n    controlId,\n    descriptionId,\n    errorId,\n    aria: {\n      id: controlId,\n      \"aria-describedby\": [descriptionId, errorId].filter(Boolean).join(\" \") || undefined,\n      \"aria-invalid\": error ? true : undefined,\n      \"aria-errormessage\": errorId,\n    },\n  };\n}\n\n/** Label above (with an optional aside at the end of its row: a counter, a link), control frame, then the message. */\nexport function Field({\n  label,\n  hideLabel,\n  description,\n  error,\n  success,\n  required,\n  controlId,\n  descriptionId,\n  errorId,\n  aside,\n  className,\n  classNames,\n  style,\n  slot = \"field\",\n  requiredOnControl,\n  children,\n}: FieldOptions & {\n  controlId: string;\n  descriptionId?: string;\n  errorId?: string;\n  /** At the end of the label's row: a character count, \"Forgot password?\", \"Optional\". */\n  aside?: ReactNode;\n  className?: string;\n  classNames?: FieldClassNames;\n  style?: CSSProperties;\n  slot?: string;\n  /** The control carries `required` itself, so the label does not repeat it in the control's name. */\n  requiredOnControl?: boolean;\n  children: ReactNode;\n}) {\n  const labelNode = label != null && (\n    <Label\n      htmlFor={controlId}\n      required={required}\n      requiredOnControl={requiredOnControl}\n      className={cn(\"text-label\", hideLabel && \"sr-only\", classNames?.label)}\n    >\n      {label}\n    </Label>\n  );\n  return (\n    <div\n      data-slot={slot}\n      data-invalid={error ? \"\" : undefined}\n      style={style}\n      className={cn(styles.field, aside != null && \"reveal-host\", classNames?.root, className)}\n    >\n      {aside != null ? (\n        <div data-slot=\"field-label-row\" className={styles.labelRow}>\n          {labelNode}\n          <span data-slot=\"field-aside\" className={cn(styles.aside, classNames?.aside)}>\n            {aside}\n          </span>\n        </div>\n      ) : (\n        labelNode\n      )}\n      {children}\n      <FieldMessage\n        error={error}\n        success={success}\n        description={description}\n        errorId={errorId}\n        descriptionId={descriptionId}\n        classNames={classNames}\n      />\n    </div>\n  );\n}\n\ntype Tone = \"error\" | \"success\" | \"description\";\ninterface Line {\n  key: string;\n  tone: Tone;\n  content: ReactNode;\n}\nconst ICON = { error: AlertCircle, success: CircleCheck } as const;\n\n/**\n * The line under a field: its problem when there is one, else its confirmation, else its help. Shared by every field\n * and choice group. A change of line keeps its path: the new line grows in under the control while the old one folds\n * away above it, so what sits below glides instead of jumping, and a quick reversal turns around from where it is.\n * The leaving line is a still copy, hidden from assistive tech and inert; ids and aria point only at the line that is\n * there. It is gone when its fold ends, and by the clock as well: a line that leaves in the frame it arrived has no\n * fold to run, so no end is reported.\n */\nexport function FieldMessage({\n  error,\n  success,\n  description,\n  errorId,\n  descriptionId,\n  slot = \"field\",\n  classNames,\n}: Pick<FieldOptions, \"error\" | \"description\" | \"success\"> & {\n  errorId?: string;\n  descriptionId?: string;\n  /** The slot names' prefix: \"checkbox\" names the lines checkbox-error, checkbox-description. */\n  slot?: string;\n  classNames?: Pick<FieldClassNames, \"error\" | \"description\" | \"success\">;\n}) {\n  const tone: Tone | null = error ? \"error\" : success ? \"success\" : description ? \"description\" : null;\n  const content = error || success || description;\n  // a new text is a new line (one problem replacing another also folds and grows); rich content changes in place\n  const text = typeof content === \"string\" || typeof content === \"number\" ? String(content) : \"\";\n  const current: Line | null = tone ? { key: `${tone}\\u0000${text}`, tone, content } : null;\n  const [last, setLast] = useState<Line | null>(current);\n  const [leaving, setLeaving] = useState<Line | null>(null);\n  // the line a field is drawn with is simply there; only a change of line moves\n  const [changed, setChanged] = useState(false);\n  if ((last?.key ?? null) !== (current?.key ?? null)) {\n    // the line that was there leaves (one at a time: a line already leaving is gone at once)\n    setLeaving(last && last.key !== current?.key ? last : null);\n    setLast(current);\n    setChanged(true);\n  }\n  const box = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    if (!leaving) return;\n    // the clock is the leaving line's own longest transition, read once as it starts to leave (the fold's end event\n    // usually comes first), so a field resolves no motion tokens of its own\n    const el = box.current?.querySelector<HTMLElement>(\"[data-leaving]\");\n    const longest = el\n      ? Math.max(\n          0,\n          ...getComputedStyle(el)\n            .transitionDuration.split(\",\")\n            .map((t) => parseFloat(t) * (t.trim().endsWith(\"ms\") ? 1 : 1000) || 0),\n        )\n      : 0;\n    const id = window.setTimeout(() => setLeaving((l) => (l === leaving ? null : l)), longest + 250);\n    return () => window.clearTimeout(id);\n  }, [leaving]);\n  if (!current && !leaving) return null;\n  const shown = leaving && leaving.key === current?.key ? null : leaving;\n  const ids = { error: errorId, success: descriptionId, description: descriptionId };\n  const line = (l: Line, gone: boolean) => {\n    const Icon = l.tone === \"description\" ? null : ICON[l.tone];\n    return (\n      <div\n        key={l.key}\n        data-slot=\"field-message-line\"\n        data-leaving={gone ? \"\" : undefined}\n        data-initial={changed ? undefined : \"\"}\n        aria-hidden={gone || undefined}\n        inert={gone || undefined}\n        className={styles.line}\n        onTransitionEnd={\n          gone\n            ? (e: TransitionEvent<HTMLDivElement>) => {\n                if (e.target === e.currentTarget && e.propertyName === \"grid-template-rows\") setLeaving(null);\n              }\n            : undefined\n        }\n      >\n        <div className={styles.clip}>\n          <p\n            id={gone ? undefined : ids[l.tone]}\n            data-slot={`${slot}-${l.tone}`}\n            className={cn(styles.message, styles[l.tone], classNames?.[l.tone])}\n          >\n            {Icon && <Icon aria-hidden className={styles.icon} />}\n            {l.content}\n          </p>\n        </div>\n      </div>\n    );\n  };\n  return (\n    <div ref={box} data-slot=\"field-message\" className={styles.messages}>\n      {shown && line(shown, true)}\n      {current && line(current, false)}\n    </div>\n  );\n}\n\n/** What the end of a text field says about its work: a spinner while a value is checked, a drawn check when it works. */\nexport type FieldStatus = \"busy\" | \"success\";\n\n/** The frame a text-like control sits in; clicking anywhere on it focuses the control. */\nexport function FieldFrame({\n  size = \"md\",\n  invalid,\n  disabled,\n  readOnly,\n  multiline,\n  status,\n  statusLabel: statusLabelProp,\n  leading,\n  trailing,\n  className,\n  classNames,\n  children,\n  onPointerDown,\n  ref,\n}: {\n  size?: FieldSize;\n  invalid?: boolean;\n  disabled?: boolean;\n  readOnly?: boolean;\n  multiline?: boolean;\n  /** busy: a spinner at the end while the value is checked or saved; success: the ring closes into a check. */\n  status?: FieldStatus;\n  /** What the spinner says while busy (\"Checking\", \"Saving\"). */\n  statusLabel?: string;\n  leading?: ReactNode;\n  trailing?: ReactNode;\n  className?: string;\n  classNames?: FieldClassNames;\n  children: ReactNode;\n  onPointerDown?: (e: ReactPointerEvent<HTMLDivElement>) => void;\n  /** The frame itself, for a control that lays out by the frame's width (useWidthAs). */\n  ref?: Ref<HTMLDivElement>;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const statusLabel = statusLabelProp === undefined ? i18n(\"statusLabel\") : statusLabelProp;\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"field-control\"\n      data-invalid={invalid ? \"\" : undefined}\n      data-disabled={disabled ? \"\" : undefined}\n      data-readonly={readOnly ? \"\" : undefined}\n      data-status={status}\n      className={cn(styles.frame, styles[size], multiline && styles.multiline, classNames?.frame, className)}\n      onPointerDown={(e) => {\n        onPointerDown?.(e);\n        // a click on the frame's padding or an adornment that is not itself interactive focuses the control\n        const t = e.target as HTMLElement;\n        if (t === e.currentTarget || (t.closest(\"[data-adornment]\") && !t.closest(\"button, a, input, [role=button]\"))) {\n          e.preventDefault();\n          e.currentTarget.querySelector<HTMLElement>(\"input, textarea\")?.focus();\n        }\n      }}\n    >\n      {leading && (\n        <span\n          data-slot=\"field-adornment\"\n          data-adornment=\"leading\"\n          className={cn(styles.adornment, classNames?.leading)}\n        >\n          {leading}\n        </span>\n      )}\n      {children}\n      {status && (\n        // one ring for both: it spins while the value is checked and closes into the check when it works\n        <span data-slot=\"field-status\" data-adornment=\"status\" className={styles.status}>\n          <Spinner size=\"sm\" state={status === \"busy\" ? \"loading\" : \"success\"} label={statusLabel} />\n        </span>\n      )}\n      {trailing && (\n        <span\n          data-slot=\"field-adornment\"\n          data-adornment=\"trailing\"\n          className={cn(styles.adornment, classNames?.trailing)}\n        >\n          {trailing}\n        </span>\n      )}\n    </div>\n  );\n}\n\n/**\n * A character count for the label's row (Field `aside`): quiet (\"58/140\") and shown while the field has attention,\n * then always shown and in words as the limit nears (\"12 left\", \"Limit reached\", \"3 over\"), so it never competes with\n * the label at rest. Assistive tech hears it only when it crosses into those words, not at every key.\n */\nexport function FieldCount({\n  length,\n  max,\n  near: nearAt,\n  slot = \"field-count\",\n  className,\n}: {\n  length: number;\n  max: number;\n  /** How many left turns it into words: by default 10 % of the limit, at least 5 (characters); 1 suits a few tags. */\n  near?: number;\n  slot?: string;\n  className?: string;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const left = max - length;\n  const near = left <= (nearAt ?? Math.max(5, Math.ceil(max * 0.1)));\n  const state = left < 0 ? \"over\" : left === 0 ? \"full\" : near ? \"near\" : \"room\";\n  const words =\n    state === \"over\"\n      ? i18n(\"fieldOver\", { count: -left })\n      : state === \"full\"\n        ? i18n(\"fieldFull\")\n        : i18n(\"fieldLeft\", { count: left });\n  return (\n    <span\n      data-slot={slot}\n      data-state={state}\n      data-shown={state === \"room\" ? undefined : \"\"}\n      className={cn(\"reveal\", styles.count, className)}\n    >\n      <span aria-hidden>{state === \"room\" ? `${i18n.number(length)}/${i18n.number(max)}` : words}</span>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {state === \"room\" ? \"\" : words}\n      </span>\n    </span>\n  );\n}\n\n/** The class a native input or textarea needs inside a FieldFrame. */\nexport const fieldControlClass = styles.control;\n\n/** What a choice group (segmented control, radio cards) spreads onto its own root when it is a form field. */\nexport interface ChoiceFieldAria {\n  \"aria-labelledby\": string;\n  \"aria-describedby\"?: string;\n  \"aria-invalid\"?: true;\n}\n\n/**\n * Whether a choice control (segmented control, radio cards, chip group) is laid out as a form field, its label shown\n * above like every field's: when asked (`field`), when it has a description, error or required mark, and whenever its\n * label is not hidden. A view switch or a quick filter passes `hideLabel`: its label stays for assistive tech only.\n * (Trials t16 and t19: agents wrote <SegmentedControl label=\"Catering needed\" …> and the question never showed.)\n */\nexport function choiceIsField(\n  field: boolean | undefined,\n  label: ReactNode,\n  hideLabel: boolean | undefined,\n  marked: boolean,\n) {\n  return field ?? (marked || (label != null && label !== \"\" && !hideLabel));\n}\n\n/**\n * A choice group as a form field: the visible label above (with the required marker), the group, then its help or its\n * problem — the same frame and message every text field has. Groups that name themselves only for assistive tech\n * (a view switch in a toolbar) do not use it.\n */\nexport function ChoiceField({\n  slot,\n  label,\n  hideLabel,\n  required,\n  description,\n  error,\n  className,\n  style,\n  classNames,\n  requiredOnControl,\n  children,\n}: Omit<FieldOptions, \"size\" | \"success\"> & {\n  slot: string;\n  /** The group carries `aria-required` itself (a radiogroup), so the label does not repeat it in the group's name. */\n  requiredOnControl?: boolean;\n  className?: string;\n  style?: CSSProperties;\n  classNames?: Pick<FieldClassNames, \"label\" | \"description\" | \"error\">;\n  children: (aria: ChoiceFieldAria) => ReactNode;\n}) {\n  const auto = useId();\n  const labelId = `choice-${auto}-label`;\n  const messageId = error || description ? `choice-${auto}-message` : undefined;\n  return (\n    <div data-slot={slot} data-invalid={error ? \"\" : undefined} style={style} className={cn(styles.field, className)}>\n      <Label\n        id={labelId}\n        required={required}\n        requiredOnControl={requiredOnControl}\n        className={cn(\"text-label\", hideLabel && \"sr-only\", classNames?.label)}\n      >\n        {label}\n      </Label>\n      {children({\n        \"aria-labelledby\": labelId,\n        ...(messageId ? { \"aria-describedby\": messageId } : {}),\n        ...(error ? { \"aria-invalid\": true as const } : {}),\n      })}\n      <FieldMessage\n        error={error}\n        description={description}\n        errorId={messageId}\n        descriptionId={messageId}\n        classNames={classNames}\n      />\n    </div>\n  );\n}\n"},{"path":"beamline/components/_shared/glide.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/glide.tsx","content":"\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type RefObject } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport overlay from \"./overlay.module.css\";\n\n/**\n * The gliding highlight for a list whose active row is tracked in state (command menu, listbox): one plate travels to\n * the active row for the arrow keys and the pointer alike, over --pui-duration-fast. It appears in place (no travel)\n * the first time and whenever `reset` changes (a new page or a new query), so typing never sends it flying.\n * The list must be the plate's offset parent (position: relative); rows stay above it (overlay.item is z-index 1).\n */\nexport function GlidePlate({\n  list,\n  selector,\n  reset,\n  axis = \"y\",\n  className,\n}: {\n  list: RefObject<HTMLElement | null>;\n  /** Finds the active row inside `list`; null when nothing is active. */\n  selector: string | null;\n  reset?: unknown;\n  /** \"y\" for a list (the plate spans the width); \"xy\" for a row or grid of cells (it takes each cell's size). */\n  axis?: \"y\" | \"xy\";\n  className?: string;\n}) {\n  const [plate, setPlate] = useState<{ x: number; y: number; w: number; h: number; jump: boolean } | null>(null);\n  const lastReset = useRef(reset);\n  const self = useRef<HTMLSpanElement>(null);\n  // eslint-disable-next-line react-hooks/exhaustive-deps -- every render on purpose; setPlate bails out when unchanged\n  useLayoutEffect(() => {\n    // the list holds the plate, and React attaches a parent's ref only after its children's layout effects: on the\n    // first commit the list is the plate's own parent (it placed itself only when something rendered again later)\n    const host = list.current ?? self.current?.parentElement;\n    const row = selector ? host?.querySelector<HTMLElement>(selector) : null;\n    const fresh = lastReset.current !== reset;\n    lastReset.current = reset;\n    if (!row) {\n      setPlate((p) => (p && p.h ? { ...p, h: 0 } : p));\n      return;\n    }\n    const x = row.offsetLeft,\n      y = row.offsetTop,\n      w = row.offsetWidth,\n      h = row.offsetHeight;\n    setPlate((p) =>\n      p && p.x === x && p.y === y && p.w === w && p.h === h && !fresh ? p : { x, y, w, h, jump: fresh || !p?.h },\n    );\n    // every render: rows can arrive or change height without the selector changing; an unchanged place keeps the state\n  });\n  return (\n    <span\n      ref={self}\n      aria-hidden\n      data-slot=\"list-highlight\"\n      className={cn(overlay.glide, className)}\n      data-on={plate?.h ? \"\" : undefined}\n      data-jump={plate?.jump ? \"\" : undefined}\n      data-axis={axis}\n      style={\n        !plate\n          ? undefined\n          : axis === \"y\"\n            ? { transform: `translateY(${plate.y}px)`, height: plate.h || undefined }\n            : { transform: `translate(${plate.x}px, ${plate.y}px)`, width: plate.w, height: plate.h || undefined }\n      }\n    />\n  );\n}\n\n/**\n * A GlidePlate for lists whose highlight is the pointer or keyboard focus rather than state (time picker, plain option\n * lists): it follows the row under the pointer or holding focus, and fades when neither is in the list. Rows drop\n * their own hover and focus tint.\n */\nexport function FollowPlate({\n  list,\n  rows,\n  className,\n}: {\n  list: RefObject<HTMLElement | null>;\n  /** Matches a row inside `list`, e.g. \"[role=option]\". */\n  rows: string;\n  className?: string;\n}) {\n  const [hot, setHot] = useState<string | null>(null);\n  // after the commit, when the list's ref is attached (a layout effect here ran before it and never listened)\n  useEffect(() => {\n    const el = list.current;\n    if (!el) return;\n    let n = 0;\n    const mark = (target: EventTarget | null) => {\n      const row = (target as HTMLElement | null)?.closest?.<HTMLElement>(rows);\n      if (!row || !el.contains(row)) return;\n      row.dataset.glideKey ??= String(++n);\n      setHot(row.dataset.glideKey);\n    };\n    const over = (e: Event) => mark(e.target);\n    const leave = () => {\n      if (!el.contains(document.activeElement)) setHot(null);\n    };\n    const blur = (e: FocusEvent) => {\n      if (!el.contains(e.relatedTarget as Node)) setHot(null);\n    };\n    el.addEventListener(\"pointerover\", over);\n    el.addEventListener(\"focusin\", over);\n    el.addEventListener(\"pointerleave\", leave);\n    el.addEventListener(\"focusout\", blur);\n    return () => {\n      el.removeEventListener(\"pointerover\", over);\n      el.removeEventListener(\"focusin\", over);\n      el.removeEventListener(\"pointerleave\", leave);\n      el.removeEventListener(\"focusout\", blur);\n    };\n  }, [list, rows]);\n  return <GlidePlate list={list} selector={hot ? `[data-glide-key=\"${hot}\"]` : null} className={className} />;\n}\n"},{"path":"beamline/components/_shared/listbox.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/listbox.tsx","content":"\"use client\";\nimport { useMessages } from \"../../runtime/localization\";\nimport { english } from \"./messages\";\n\nimport { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { Highlight, filterRanked } from \"./match\";\nimport { cn } from \"../../lib/utils\";\nimport { GlidePlate } from \"./glide\";\nimport overlay from \"./overlay.module.css\";\n\nexport interface ListOption {\n  value: string;\n  label: string;\n  description?: string;\n  /** Other words that should find it. */\n  keywords?: string[];\n  icon?: ReactNode;\n  disabled?: boolean;\n  /** Heading it sits under (Billing address). With no query the list shows a heading where the group changes, in the\n   * given order; while searching, results rank flat and each row names its group beside its description. */\n  group?: string;\n}\n\n/** Lists longer than this mount only the rows near the visible window. */\nconst WINDOW = 200;\n/** A one-line row's height before it is measured, and rows kept mounted past each edge. */\nconst ROW = 34;\nconst OVERSCAN = 8;\n\n/** Filtering and the active row for a search-as-you-type list; the field keeps focus and points at the active row. */\nexport function useListbox(options: ListOption[], query: string, extra?: ListOption | null) {\n  // a group's name finds its options too; `extra` (a row that acts, such as Create \"…\") always comes last\n  const ranked = useMemo(\n    () => filterRanked(options, query, (o) => (o.group ? { ...o, keywords: [...(o.keywords ?? []), o.group] } : o)),\n    [options, query],\n  );\n  const filtered = useMemo(() => (extra ? [...ranked, extra] : ranked), [ranked, extra]);\n  const [active, setActive] = useState(0);\n  const viaKeys = useRef(false);\n  useEffect(() => {\n    setActive(\n      Math.max(\n        0,\n        filtered.findIndex((o) => !o.disabled),\n      ),\n    );\n  }, [query]); // eslint-disable-line react-hooks/exhaustive-deps -- reset per question\n  const move = (delta: number) => {\n    if (!filtered.length) return;\n    viaKeys.current = true;\n    let i = active;\n    for (let n = 0; n < filtered.length; n++) {\n      i = (i + delta + filtered.length) % filtered.length;\n      if (!filtered[i].disabled) break;\n    }\n    setActive(i);\n  };\n  // Home, End and Page keys: to a place in the whole list (no wrap), the nearest enabled row from there\n  const jump = (to: number) => {\n    if (!filtered.length) return;\n    viaKeys.current = true;\n    const at = Math.max(0, Math.min(filtered.length - 1, to));\n    const back = to < active;\n    for (let n = 0; n < filtered.length; n++) {\n      const i = back ? at + n : at - n;\n      if (i >= 0 && i < filtered.length && !filtered[i].disabled) return setActive(i);\n    }\n  };\n  return {\n    filtered,\n    active: Math.min(active, filtered.length - 1),\n    setActive,\n    move,\n    jump,\n    viaKeys,\n    current: filtered[Math.min(active, filtered.length - 1)],\n  };\n}\n\n/** The list itself: rows in the system's menu look, the match emphasised, chosen rows checked. */\nexport function Listbox({\n  id,\n  label,\n  options,\n  query,\n  active,\n  onActive,\n  isSelected,\n  onPick,\n  multiple,\n  emptyText,\n  viaKeys,\n  className,\n}: {\n  id: string;\n  label: string;\n  options: ListOption[];\n  query: string;\n  active: number;\n  onActive: (i: number) => void;\n  isSelected: (value: string) => boolean;\n  onPick: (option: ListOption) => void;\n  multiple?: boolean;\n  emptyText?: ReactNode;\n  viaKeys: { current: boolean };\n  className?: string;\n}) {\n  const i18n = useMessages(\"_shared\", english);\n  const list = useRef<HTMLDivElement>(null);\n  // a long list mounts only the rows near the visible window; spacers above and below keep its scroll height true\n  const windowed = options.length > WINDOW;\n  const heights = useRef(new Map<string, number>());\n  const [scroll, setScroll] = useState({ top: 0, height: 300 });\n  const [measured, setMeasured] = useState(0);\n  const offsets = useMemo(() => {\n    if (!windowed) return null;\n    const out = new Float64Array(options.length + 1);\n    for (let i = 0; i < options.length; i++)\n      out[i + 1] = out[i] + (heights.current.get(options[i].value) ?? (options[i].description ? 46 : ROW));\n    return out;\n  }, [windowed, options, measured]); // eslint-disable-line react-hooks/exhaustive-deps -- measured invalidates the cached heights\n  // the first row whose bottom is below y\n  const rowAt = (y: number) => {\n    let lo = 0;\n    let hi = options.length;\n    while (lo < hi) {\n      const mid = (lo + hi) >> 1;\n      if (offsets![mid + 1] <= y) lo = mid + 1;\n      else hi = mid;\n    }\n    return lo;\n  };\n  const first = windowed ? Math.max(0, rowAt(scroll.top) - OVERSCAN) : 0;\n  const last = windowed ? Math.min(options.length, rowAt(scroll.top + scroll.height) + OVERSCAN + 1) : options.length;\n  // the mounted rows measure once each time the window moves; a row only re-renders when a height changed\n  useLayoutEffect(() => {\n    if (!windowed || !list.current) return;\n    let changed = false;\n    for (const el of list.current.querySelectorAll<HTMLElement>(\"[data-row]\")) {\n      const h = el.offsetHeight;\n      if (h && heights.current.get(el.dataset.row!) !== h) {\n        heights.current.set(el.dataset.row!, h);\n        changed = true;\n      }\n    }\n    if (changed) setMeasured((m) => m + 1);\n  }, [windowed, first, last, options]);\n  useEffect(() => {\n    if (!viaKeys.current) return;\n    const row = list.current?.querySelector(`#${CSS.escape(`${id}-${active}`)}`);\n    if (row) row.scrollIntoView({ block: \"nearest\" });\n    else if (offsets && list.current) {\n      // not mounted: scroll its place into view and the row renders there\n      const el = list.current;\n      if (offsets[active] < el.scrollTop) el.scrollTop = offsets[active];\n      else if (offsets[active + 1] > el.scrollTop + el.clientHeight)\n        el.scrollTop = offsets[active + 1] - el.clientHeight;\n    }\n  }, [active, id, viaKeys, offsets]);\n  // what a search found, said once it settles (a polite status beside the list, never inside it: a listbox holds\n  // options only), so a screen reader hears \"3 results\" or that nothing matched while focus stays in the field\n  const found = !query.trim()\n    ? \"\"\n    : options.length\n      ? i18n(\"results\", { count: options.length })\n      : typeof emptyText === \"string\"\n        ? emptyText\n        : i18n(\"noMatch\", { query });\n  return (\n    <>\n      <span role=\"status\" className=\"sr-only\" data-slot=\"listbox-status\">\n        {found}\n      </span>\n      <div\n        ref={list}\n        id={id}\n        role=\"listbox\"\n        aria-label={label}\n        aria-multiselectable={multiple || undefined}\n        onPointerMove={() => {\n          viaKeys.current = false;\n        }}\n        onScroll={\n          windowed\n            ? (e) => setScroll({ top: e.currentTarget.scrollTop, height: e.currentTarget.clientHeight })\n            : undefined\n        }\n        className={cn(\n          \"relative max-h-[min(300px,var(--radix-popover-content-available-height,300px))] overflow-y-auto p-1\",\n          className,\n        )}\n      >\n        <GlidePlate list={list} selector={options.length ? '[role=\"option\"][data-active]' : null} reset={query} />\n        {options.length === 0 && (\n          <div className=\"px-3 py-6 text-center text-sm text-ink-3\">{emptyText ?? i18n(\"noMatch\", { query })}</div>\n        )}\n        {windowed && <div aria-hidden style={{ height: offsets![first] }} data-slot=\"listbox-spacer\" />}\n        {options.slice(first, last).map((o, k) => {\n          const i = first + k;\n          const on = isSelected(o.value);\n          const prev = options[i - 1];\n          // headings only for the unsearched list, where they follow the caller's order; arrow keys never land on them\n          const groupStart = !query && (o.group ?? \"\") !== (prev?.group ?? \"\") && (i > 0 || !!o.group);\n          const detail = query && o.group ? [o.group, o.description].filter(Boolean).join(\" · \") : o.description;\n          const row = (\n            <>\n              {groupStart && i > 0 && (\n                <div role=\"presentation\" data-slot=\"listbox-separator\" className={overlay.separator} />\n              )}\n              {groupStart && o.group && (\n                <div role=\"presentation\" data-slot=\"listbox-group\" className={overlay.heading}>\n                  {o.group}\n                </div>\n              )}\n              <div\n                id={`${id}-${i}`}\n                role=\"option\"\n                aria-selected={on}\n                aria-disabled={o.disabled || undefined}\n                aria-description={!query && o.group ? o.group : undefined}\n                aria-setsize={windowed ? options.length : undefined}\n                aria-posinset={windowed ? i + 1 : undefined}\n                data-disabled={o.disabled ? \"\" : undefined}\n                data-active={i === active ? \"\" : undefined}\n                className={cn(overlay.item, \"cursor-pointer\", detail && \"py-1.5\")}\n                onPointerMove={() => {\n                  if (i !== active && !o.disabled) onActive(i);\n                }}\n                onMouseDown={(e) => e.preventDefault()}\n                onClick={() => {\n                  if (!o.disabled) onPick(o);\n                }}\n              >\n                {/* a checked row fills at once and its check scales in (a fade under reduced motion); unchecking\n                  lets the fill go over --pui-motion-out, like every hover */}\n                {multiple && (\n                  <span\n                    aria-hidden\n                    data-slot=\"listbox-check\"\n                    className={cn(\n                      \"grid size-4 shrink-0 place-items-center rounded-[calc(var(--pui-radius-chip)-2px)] shadow-[inset_0_0_0_1px_var(--pui-line-strong)] transition-[background-color,box-shadow] duration-[var(--pui-motion-out)]\",\n                      on && \"bg-accent text-[var(--pui-accent-foreground)] shadow-none duration-0\",\n                    )}\n                  >\n                    {on && <Check className=\"!size-3 animate-enter-scale !text-current\" strokeWidth={3} />}\n                  </span>\n                )}\n                {o.icon}\n                <span className=\"grid min-w-0 flex-1 gap-px leading-snug\">\n                  <span className=\"truncate\">\n                    <Highlight text={o.label} query={query} />\n                  </span>\n                  {detail && <span className=\"truncate text-xs text-ink-3\">{detail}</span>}\n                </span>\n                {!multiple && on && <Check data-slot=\"listbox-check\" className=\"animate-enter-scale !text-accent\" />}\n              </div>\n            </>\n          );\n          // a windowed row is measured with its heading, so the spacers stay true\n          return windowed ? (\n            <div key={o.value} role=\"presentation\" data-row={o.value}>\n              {row}\n            </div>\n          ) : (\n            <Fragment key={o.value}>{row}</Fragment>\n          );\n        })}\n        {windowed && (\n          <div aria-hidden style={{ height: offsets![options.length] - offsets![last] }} data-slot=\"listbox-spacer\" />\n        )}\n      </div>\n    </>\n  );\n}\n"},{"path":"beamline/components/_shared/match.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/match.tsx","content":"/** One way to rank and mark matches for every search-as-you-type list (command menu, combobox, multi-select). */\n\n/** How well `label` (and its keywords) answers `query`: label start 100, word start 80, anywhere 60, keyword 45,\n *  every word of a several-word query found across label, keywords and description 40 (50 when one starts the label:\n *  \"billing city\" finds Billing address › City), description 30, letters in order 15, no match 0. An empty query\n *  matches everything with 1. */\nexport function rank(query: string, label: string, keywords?: string[], description?: string): number {\n  const q = query.trim().toLowerCase();\n  if (!q) return 1;\n  const l = label.toLowerCase();\n  const at = l.indexOf(q);\n  if (at === 0) return 100 - l.length / 100;\n  if (at > 0 && /[\\s\\-/(_.]/.test(l[at - 1])) return 80;\n  if (at > 0) return 60;\n  if (keywords?.some((k) => k.toLowerCase().includes(q))) return 45;\n  const words = q.split(/\\s+/);\n  if (words.length > 1) {\n    const hay = [l, ...(keywords ?? []).map((k) => k.toLowerCase()), description?.toLowerCase() ?? \"\"];\n    if (words.every((w) => hay.some((h) => h.includes(w)))) return words.some((w) => l.startsWith(w)) ? 50 : 40;\n  }\n  if (description?.toLowerCase().includes(q)) return 30;\n  let i = 0;\n  for (const ch of l) if (ch === q[i]) i++;\n  return i === q.length ? 15 : 0;\n}\n\n/** Sorts by rank (best first, original order for ties) and drops non-matches. */\nexport function filterRanked<T>(\n  items: T[],\n  query: string,\n  get: (item: T) => { label: string; keywords?: string[]; description?: string },\n): T[] {\n  if (!query.trim()) return items;\n  return items\n    .map((item, order) => {\n      const g = get(item);\n      return { item, order, s: rank(query, g.label, g.keywords, g.description) };\n    })\n    .filter((r) => r.s > 0)\n    .sort((a, b) => b.s - a.s || a.order - b.order)\n    .map((r) => r.item);\n}\n\n/** The label with the matched run emphasised. */\nexport function Highlight({ text, query }: { text: string; query: string }) {\n  const q = query.trim().toLowerCase();\n  const at = q ? text.toLowerCase().indexOf(q) : -1;\n  if (at < 0) return <>{text}</>;\n  return (\n    <>\n      {text.slice(0, at)}\n      <mark className=\"bg-transparent font-medium text-ink\">{text.slice(at, at + q.length)}</mark>\n      {text.slice(at + q.length)}\n    </>\n  );\n}\n"},{"path":"beamline/components/_shared/messages.ts","type":"registry:file","target":"src/ui/beamline/components/_shared/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy for this helper only. */\nexport const english = {\n  results: {\n    one: \"{count} result\",\n    other: \"{count} results\",\n  },\n  noMatch: \"No match for {query}\",\n  fieldOver: \"{count} over\",\n  fieldFull: \"Limit reached\",\n  fieldLeft: \"{count} left\",\n  foldMore: \"{list} and {count} more\",\n  foldShow: \"Show {count} more {noun}: {list}\",\n  statusLabel: \"Checking\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/_shared/overlay.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/overlay.module.css","content":"@layer properties, theme, base, components, utilities;\n/* the source morph's moving parts: the clip's four sides, the radius and the offset back to the trigger */\n@property --ci-t {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-r {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-b {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-l {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-x {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n@property --ci-y {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n@layer components {\n  /* Every floating layer in the system: one surface (painted by the overlay role of the look in force), one menu-item\n   look, one scrim, one enter and exit. A layer placed against a trigger opens out of it (the source morph below); one\n   without a placement arrives in --pui-motion-in scaling from its origin. Each leaves in --pui-motion-out; Radix keeps\n   it mounted until the exit animation ends. Reduced motion makes both instant (the duration tokens become 1ms). */\n  .surface {\n    z-index: var(--pui-z-popover);\n    min-width: 8rem;\n    /* never past the screen: as wide and as tall as the room beside the trigger (Radix measures it) */\n    max-width: var(--radix-popper-available-width, none);\n    max-height: var(--radix-popper-available-height, 70vh);\n    overflow: auto;\n    border-radius: var(--pui-overlay-radius, var(--pui-radius-card));\n    color: var(--pui-ink);\n    transform-origin: var(--radix-popper-transform-origin, var(--radix-dropdown-menu-content-transform-origin, center));\n    outline: none;\n  }\n  .surface[data-state=\"open\"],\n  .surface[data-state=\"delayed-open\"],\n  .surface[data-state=\"instant-open\"] {\n    animation: pop-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  .surface[data-state=\"closed\"] {\n    animation: pop-out var(--pui-motion-out) var(--pui-ease-default) forwards;\n  }\n\n  /* The source morph (research/morphing-interfaces-2026-10-04, M3): a layer placed against a trigger does not fade in\n     beside it, it opens out of it. It starts as the trigger's own box (Radix gives its size and the side and alignment\n     it was placed on), offset back onto the trigger, and grows: the side nearest the trigger is there at once, the\n     sides across follow, the far side arrives last (~200 ms), so the rows by the trigger are readable within the\n     ruled 90 ms while the outline settles. Content fades in over those 90 ms. It closes back into the trigger in the\n     ruled 180 ms. The clip ends 48px outside the box so the layer's shadow is never cut. Reduced motion: instant. */\n  .surface[data-side] {\n    --_aw: var(--radix-popper-anchor-width, 0px);\n    --_ah: var(--radix-popper-anchor-height, 0px);\n    --_gap: 6px;\n    --from-t: -48px;\n    --from-r: -48px;\n    --from-b: -48px;\n    --from-l: -48px;\n    --from-x: 0px;\n    --from-y: 0px;\n    --d-near: calc(var(--pui-duration-standard) * 0.55);\n    --d-cross: calc(var(--pui-duration-standard) * 0.85);\n    --d-far: var(--pui-duration-standard);\n    clip-path: inset(\n      var(--ci-t) var(--ci-r) var(--ci-b) var(--ci-l) round var(--pui-overlay-radius, var(--pui-radius-card))\n    );\n    translate: var(--ci-x) var(--ci-y);\n  }\n  .surface[data-side=\"bottom\"] {\n    --from-t: 0px;\n    --from-b: calc(100% - var(--_ah));\n    --from-y: calc(-1 * (var(--_ah) + var(--_gap)));\n    --d-t: var(--d-near);\n    --d-b: var(--d-far);\n    --d-l: var(--d-cross);\n    --d-r: var(--d-cross);\n  }\n  .surface[data-side=\"top\"] {\n    --from-b: 0px;\n    --from-t: calc(100% - var(--_ah));\n    --from-y: calc(var(--_ah) + var(--_gap));\n    --d-b: var(--d-near);\n    --d-t: var(--d-far);\n    --d-l: var(--d-cross);\n    --d-r: var(--d-cross);\n  }\n  .surface[data-side=\"right\"] {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n    --from-x: calc(-1 * (var(--_aw) + var(--_gap)));\n    --d-l: var(--d-near);\n    --d-r: var(--d-far);\n    --d-t: var(--d-cross);\n    --d-b: var(--d-cross);\n  }\n  .surface[data-side=\"left\"] {\n    --from-r: 0px;\n    --from-l: calc(100% - var(--_aw));\n    --from-x: calc(var(--_aw) + var(--_gap));\n    --d-r: var(--d-near);\n    --d-l: var(--d-far);\n    --d-t: var(--d-cross);\n    --d-b: var(--d-cross);\n  }\n  /* alignment along the trigger: which part of the layer the trigger sits over */\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"start\"] {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"end\"] {\n    --from-l: calc(100% - var(--_aw));\n    --from-r: 0px;\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"start\"]:dir(rtl) {\n    --from-l: calc(100% - var(--_aw));\n    --from-r: 0px;\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"end\"]:dir(rtl) {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"center\"] {\n    --from-l: calc((100% - var(--_aw)) / 2);\n    --from-r: calc((100% - var(--_aw)) / 2);\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"start\"] {\n    --from-t: 0px;\n    --from-b: calc(100% - var(--_ah));\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"end\"] {\n    --from-t: calc(100% - var(--_ah));\n    --from-b: 0px;\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"center\"] {\n    --from-t: calc((100% - var(--_ah)) / 2);\n    --from-b: calc((100% - var(--_ah)) / 2);\n  }\n  .surface[data-side]:is([data-state=\"open\"], [data-state=\"delayed-open\"], [data-state=\"instant-open\"]) {\n    animation:\n      grow-t var(--d-t) var(--pui-ease-out-strong),\n      grow-r var(--d-r) var(--pui-ease-out-strong),\n      grow-b var(--d-b) var(--pui-ease-out-strong),\n      grow-l var(--d-l) var(--pui-ease-out-strong),\n      grow-xy var(--d-cross) var(--pui-ease-out-strong),\n      settling var(--d-far) step-end;\n  }\n  .surface[data-side]:is([data-state=\"open\"], [data-state=\"delayed-open\"], [data-state=\"instant-open\"]) > * {\n    animation: content-in var(--pui-motion-in) var(--pui-ease-default) both;\n  }\n  /* one duration for every part of the exit: the layer unmounts when the first of them ends */\n  .surface[data-side][data-state=\"closed\"] {\n    animation: shrink var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  .surface[data-side][data-state=\"closed\"] > * {\n    animation: content-out var(--pui-motion-in) var(--pui-ease-default) forwards;\n  }\n  @keyframes grow-t {\n    from {\n      --ci-t: var(--from-t);\n    }\n  }\n  @keyframes grow-r {\n    from {\n      --ci-r: var(--from-r);\n    }\n  }\n  @keyframes grow-b {\n    from {\n      --ci-b: var(--from-b);\n    }\n  }\n  @keyframes grow-l {\n    from {\n      --ci-l: var(--from-l);\n    }\n  }\n  @keyframes grow-xy {\n    from {\n      --ci-x: var(--from-x);\n      --ci-y: var(--from-y);\n    }\n  }\n  /* it starts over its own trigger: until it has grown, the press that opened it must not land on a row */\n  @keyframes settling {\n    0%,\n    100% {\n      pointer-events: none;\n    }\n  }\n  @keyframes shrink {\n    to {\n      --ci-t: var(--from-t);\n      --ci-r: var(--from-r);\n      --ci-b: var(--from-b);\n      --ci-l: var(--from-l);\n      --ci-x: var(--from-x);\n      --ci-y: var(--from-y);\n      opacity: 0;\n    }\n  }\n  @keyframes content-in {\n    from {\n      opacity: 0;\n    }\n  }\n  @keyframes content-out {\n    to {\n      opacity: 0;\n    }\n  }\n  .padded {\n    padding: var(--pui-pad-popover);\n  }\n  .menu {\n    padding: var(--pui-space-1);\n  }\n  @keyframes pop-in {\n    from {\n      opacity: 0;\n      transform: scale(0.97);\n    }\n  }\n  @keyframes pop-out {\n    to {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n\n  /* rows (and the gliding plate) nest in the list: its radius minus its padding, never rounder than a chip; on the\n     row itself, so any list that uses .item gets it */\n  .item,\n  .glide {\n    --_item-radius: min(\n      var(--pui-radius-chip),\n      calc(var(--pui-overlay-radius, var(--pui-radius-card)) - var(--pui-space-1))\n    );\n  }\n\n  /* menu rows: shared by dropdown, context menu, menubar, select, combobox lists */\n  .item {\n    position: relative;\n    z-index: 1;\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-2);\n    min-height: var(--pui-control-height-sm);\n    padding: 0 var(--pui-space-2);\n    /* nested in the list: the overlay's radius minus its padding, never rounder than a chip */\n    border-radius: var(--_item-radius);\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    cursor: default;\n    user-select: none;\n    outline: none;\n  }\n  /* on a touch screen a row is a finger's target: the touch height (44px) from the tokens; a mouse keeps the dense row */\n  @media (pointer: coarse) {\n    .item {\n      min-height: var(--pui-control-height-touch);\n    }\n  }\n  /* a destructive row speaks in the danger ink only while it can be used: disabled wins (muted ink, no alarm for an\n     action that cannot happen) */\n  .item[data-tone=\"danger\"]:not([data-disabled]) {\n    color: var(--pui-red);\n  }\n  .item[data-disabled] {\n    color: var(--pui-ink-3);\n    pointer-events: none;\n  }\n  .item :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n  }\n  .item[data-tone=\"danger\"]:not([data-disabled]) :where(svg) {\n    color: currentColor;\n  }\n  .item[data-highlighted]:not([data-glide]) {\n    background: var(--pui-hover-2);\n  }\n  /* a press is acknowledged at once, over the plate, before the menu closes or the row toggles */\n  .item:active:not([data-disabled]) {\n    background: var(--pui-hover);\n  }\n  .shortcut {\n    margin-inline-start: auto;\n    padding-inline-start: var(--pui-space-4);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    letter-spacing: var(--pui-tracking-mono-caps);\n  }\n  .heading {\n    padding: var(--pui-space-1-5) var(--pui-space-2) var(--pui-space-1);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-medium);\n  }\n  .separator {\n    height: 1px;\n    margin: var(--pui-space-1) calc(var(--pui-space-1) * -1);\n    background: var(--pui-line);\n  }\n  .check {\n    margin-inline-start: auto;\n    color: var(--pui-accent) !important;\n  }\n  /* a check arrives in its row as the value changes (at once): it grows from the middle over the arrival */\n  .check :where(svg) {\n    animation: check-in var(--pui-motion-in) var(--pui-ease-out-strong);\n  }\n  @keyframes check-in {\n    from {\n      opacity: 0;\n      scale: calc(1 - 0.5 * var(--pui-motion-travel, 1));\n    }\n  }\n  /* the gliding highlight: one plate moves between rows for the pointer and the arrow keys; it appears in place */\n  .glide {\n    position: absolute;\n    z-index: 0;\n    inset-inline: var(--pui-space-1);\n    top: 0;\n    border-radius: var(--_item-radius);\n    background: var(--pui-hover-2);\n    pointer-events: none;\n    opacity: 0;\n    transition: opacity var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .glide[data-on] {\n    opacity: 1;\n    transition:\n      transform var(--pui-duration-fast) var(--pui-ease-standard),\n      height var(--pui-duration-fast) var(--pui-ease-standard),\n      opacity 0ms;\n  }\n  .glide[data-jump] {\n    transition: opacity 0ms;\n  }\n  /* a plate over a row or grid of cells takes each cell's size and travels both ways */\n  .glide[data-axis=\"xy\"] {\n    inset-inline: auto;\n    left: 0;\n  }\n  .glide[data-axis=\"xy\"][data-on] {\n    transition:\n      transform var(--pui-duration-fast) var(--pui-ease-standard),\n      width var(--pui-duration-fast) var(--pui-ease-standard),\n      height var(--pui-duration-fast) var(--pui-ease-standard),\n      opacity 0ms;\n  }\n\n  /* a layer that has begun to close is out of the way at once: for the length of its exit it takes no pointer, so a tap\n     meant for the page behind (the next row, the button the panel covered) lands there, never on the leaving ghost.\n     Focus has already gone back (Radix) and it unmounts when the exit ends. */\n  .scrim[data-state=\"closed\"],\n  .surface[data-state=\"closed\"],\n  .dialog[data-state=\"closed\"],\n  .sheet[data-state=\"closed\"] {\n    pointer-events: none;\n  }\n\n  /* modal layers */\n  .scrim {\n    position: fixed;\n    inset: 0;\n    z-index: var(--pui-z-overlay);\n    background: var(--pui-scrim);\n    backdrop-filter: blur(var(--pui-blur-overlay));\n  }\n  .scrim[data-state=\"open\"] {\n    animation: fade-in var(--pui-fade-in) var(--pui-ease-default);\n  }\n  .scrim[data-state=\"closed\"] {\n    animation: fade-out var(--pui-fade-out) var(--pui-ease-default) forwards;\n  }\n  @keyframes fade-in {\n    from {\n      opacity: 0;\n    }\n  }\n  @keyframes fade-out {\n    to {\n      opacity: 0;\n    }\n  }\n  .dialog {\n    /* the content answers to the dialog's own width (a footer's buttons share the row on a phone) */\n    container-type: inline-size;\n    position: fixed;\n    z-index: var(--pui-z-modal);\n    left: 50%;\n    top: 50%;\n    width: min(calc(100vw - var(--pui-space-8)), var(--pui-dialog-width, var(--_dialog-width, 440px)));\n    max-height: calc(100dvh - var(--pui-space-12));\n    overflow: hidden;\n    translate: -50% -50%;\n    border-radius: var(--pui-dialog-radius, var(--pui-radius-window));\n    color: var(--pui-ink);\n    outline: none;\n    /* a size chosen while open (a step that needs more room) eases the width in place, centred */\n    transition: width var(--pui-duration-standard) var(--pui-ease-standard);\n  }\n  .dialog[data-state=\"open\"] {\n    animation: dialog-in var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .dialog[data-state=\"closed\"] {\n    animation: dialog-out var(--pui-motion-out) var(--pui-ease-default) forwards;\n  }\n  /* A dialog opened from something (use-open-from.ts) grows out of it: it starts as the opener's box, offset back onto\n     it, and grows to its own place over the considered duration while its content fades in; it closes back into the\n     opener in the ruled 180 ms. Shares the source morph's moving parts and guard with .surface. */\n  .dialog[data-open-from] {\n    clip-path: inset(\n      var(--ci-t) var(--ci-r) var(--ci-b) var(--ci-l) round var(--pui-dialog-radius, var(--pui-radius-window))\n    );\n    transform: translate(var(--ci-x), var(--ci-y));\n  }\n  .dialog[data-open-from][data-state=\"open\"] {\n    animation:\n      grow-t var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-r var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-b var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-l var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-xy calc(var(--pui-duration-considered) * 0.9) var(--pui-ease-out-strong),\n      settling var(--pui-duration-considered) step-end;\n  }\n  .dialog[data-open-from][data-state=\"open\"] > * {\n    animation: content-in var(--pui-duration-standard) var(--pui-ease-default) calc(var(--pui-duration-standard) * 0.35)\n      both;\n  }\n  .dialog[data-open-from][data-state=\"closed\"] {\n    animation: shrink var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  .dialog[data-open-from][data-state=\"closed\"] > * {\n    animation: content-out var(--pui-motion-in) var(--pui-ease-default) forwards;\n  }\n  /* A sheet: a panel from an edge. It slides in from its edge over the considered duration on the drawer curve and\n     goes back out to that edge in the ruled departure (a layer closes back into where it came from). The edge is\n     logical: an end (\"right\") sheet comes from the left in a right-to-left page. Its content answers to its width. */\n  .sheet {\n    --_sx: 0px;\n    --_sy: 0px;\n    container-type: inline-size;\n    /* where a finger has swiped it toward its edge (sheet.tsx); it settles back over the departure */\n    translate: var(--_swipe, 0px) 0;\n  }\n  /* a side panel leaves sideways pans to the swipe and keeps vertical scrolling native, in its body too */\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]),\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]) [data-slot=\"sheet-body\"] {\n    touch-action: pan-y;\n  }\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]) {\n    transition:\n      var(--m-transition),\n      translate var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .sheet[data-swiping] {\n    transition: none;\n  }\n  .sheet[data-side=\"right\"] {\n    --_sx: 100%;\n  }\n  .sheet[data-side=\"left\"] {\n    --_sx: -100%;\n  }\n  .sheet[data-side=\"right\"]:dir(rtl) {\n    --_sx: -100%;\n  }\n  .sheet[data-side=\"left\"]:dir(rtl) {\n    --_sx: 100%;\n  }\n  .sheet[data-side=\"top\"] {\n    --_sy: -100%;\n  }\n  .sheet[data-side=\"bottom\"] {\n    --_sy: 100%;\n  }\n  .sheet[data-state=\"open\"] {\n    animation: sheet-in var(--pui-duration-considered) var(--pui-ease-drawer) both;\n  }\n  .sheet[data-state=\"closed\"] {\n    animation: sheet-out var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  @keyframes sheet-in {\n    from {\n      translate: var(--_sx) var(--_sy);\n    }\n  }\n  @keyframes sheet-out {\n    to {\n      translate: var(--_sx) var(--_sy);\n    }\n  }\n  @keyframes dialog-in {\n    from {\n      opacity: 0;\n      transform: scale(0.96);\n    }\n  }\n  @keyframes dialog-out {\n    to {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n}\n\n/* Reduced motion: surfaces and dialogs fade without scaling or growing, over the fade tokens (restrained under reduced,\n   1ms under none; APPEARANCE A06). */\n@media (prefers-reduced-motion: reduce) {\n  .surface,\n  .dialog,\n  .sheet {\n    animation-name: fade-in !important;\n    animation-duration: var(--pui-fade-in) !important;\n  }\n  .surface[data-state=\"closed\"],\n  .dialog[data-state=\"closed\"],\n  .sheet[data-state=\"closed\"] {\n    animation-name: fade-out !important;\n    animation-duration: var(--pui-fade-out) !important;\n  }\n}\n@layer components {\n  :global(.pui[data-motion=\"reduced\"]) .surface,\n  :global(.pui[data-motion=\"none\"]) .surface,\n  :global(.pui[data-motion=\"reduced\"]) .dialog,\n  :global(.pui[data-motion=\"none\"]) .dialog,\n  :global(.pui[data-motion=\"reduced\"]) .sheet,\n  :global(.pui[data-motion=\"none\"]) .sheet {\n    animation-name: fade-in;\n    animation-duration: var(--pui-fade-in);\n  }\n  :global(.pui[data-motion=\"reduced\"]) .surface[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .surface[data-state=\"closed\"],\n  :global(.pui[data-motion=\"reduced\"]) .dialog[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .dialog[data-state=\"closed\"],\n  :global(.pui[data-motion=\"reduced\"]) .sheet[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .sheet[data-state=\"closed\"] {\n    animation-name: fade-out;\n    animation-duration: var(--pui-fade-out);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.surface {\n  composes: m-overlay from global;\n}\n.dialog {\n  composes: m-overlay from global;\n}\n"},{"path":"beamline/components/_shared/text-action.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/text-action.module.css","content":"@layer properties, theme, base, components, utilities;\n/* An action that reads as words in the flow of a form or a sentence: \"Use another account\", \"Resend code\", \"Select\n   all\", \"Show fewer\". One recipe so every one of them answers alike; a framed action is a Button. */\n@layer components {\n  .root {\n    border-radius: var(--pui-radius-chip);\n    text-underline-offset: 0.25em;\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root:hover {\n    transition-duration: 0ms;\n  }\n  .root:disabled {\n    pointer-events: none;\n    opacity: var(--pui-opacity-disabled);\n  }\n  /* accent: the way forward out of the form (a link's colour, underlined on hover) */\n  .root[data-tone=\"accent\"] {\n    color: var(--pui-accent-ink);\n    font-weight: var(--pui-weight-medium);\n  }\n  .root[data-tone=\"accent\"]:hover {\n    text-decoration-line: underline;\n  }\n  /* quiet: a side action in the caller's ink that takes full ink on hover */\n  .root[data-tone=\"quiet\"]:hover:not(:disabled) {\n    color: var(--pui-ink);\n  }\n  /* inline: inside a sentence, underlined at rest so it reads as a link without colour */\n  .root[data-tone=\"inline\"] {\n    text-decoration-line: underline;\n  }\n}\n"},{"path":"beamline/components/_shared/text-action.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/text-action.tsx","content":"\"use client\";\n// organism-allow-file: raw-control -- the shared part every element draws this control from\n\nimport type { ButtonHTMLAttributes, Ref } from \"react\";\nimport * as Slot from \"@radix-ui/react-slot\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./text-action.module.css\";\n\nexport interface TextActionProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  /** accent: the way forward (a link's colour, underlined on hover); quiet: a side action in the surrounding ink that\n   *  takes full ink on hover; inline: inside a sentence, underlined at rest. */\n  tone?: \"accent\" | \"quiet\" | \"inline\";\n  /** Render the single child (a link) with the action's look instead of a <button>. */\n  asChild?: boolean;\n  ref?: Ref<HTMLButtonElement>;\n}\n\n/** An action that reads as words, not a framed control: \"Resend code\", \"Select all\", \"Show fewer\". */\nexport function TextAction({ tone = \"accent\", asChild, type = \"button\", className, ref, ...props }: TextActionProps) {\n  const shared = { \"data-tone\": tone, className: cn(styles.root, className) };\n  if (asChild) return <Slot.Root ref={ref} {...shared} {...props} />;\n  return <button ref={ref} type={type} {...shared} {...props} />;\n}\n"},{"path":"beamline/components/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/bezel/bezel.module.css","type":"registry:file","target":"src/ui/beamline/components/bezel/bezel.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* the shell is a surface and the inner panel a well, so the double bezel takes the look's paint */\n  .shell {\n    --pui-gap: var(--pui-bezel-gap);\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    gap: var(--pui-gap);\n    min-width: 0;\n    padding: var(--pui-gap);\n    border-radius: var(--pui-bezel-radius);\n  }\n  .shell[data-size=\"sm\"] {\n    --pui-gap: var(--pui-space-1-5);\n    border-radius: calc(var(--pui-bezel-radius) - var(--pui-gap));\n  }\n  /* The inner radius is the outer minus the gap, plus a little: parallel curves read as one object. */\n  .inner {\n    position: relative;\n    min-width: 0;\n    min-height: 0;\n    /* grow into a shell given a height, but keep a size set on the panel (flex-basis auto, not 0) */\n    flex: 1 1 auto;\n    overflow: hidden;\n    border-radius: calc(var(--pui-bezel-radius) - var(--pui-gap) + var(--pui-space-0-5));\n  }\n  .shell[data-size=\"sm\"] > .inner {\n    border-radius: calc(var(--pui-bezel-radius) - 2 * var(--pui-gap) + var(--pui-space-0-5));\n  }\n  .inner > :is(img, video, canvas) {\n    display: block;\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n  }\n  /* interactive: the picture set into the panel leans toward the pointer (a small grow, clipped by the panel, so the\n     frame never moves) and settles back when it leaves; focus inside does the same, so the keyboard sees it too.\n     Transform only; reduced motion keeps the frame still. */\n  .shell[data-interactive] {\n    cursor: pointer;\n  }\n  .shell[data-interactive] > .inner > * {\n    transition: scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .shell[data-interactive]:is(:hover, :focus-within) > .inner > * {\n    scale: calc(1 + 0.04 * var(--pui-motion-travel, 1));\n    transition-duration: var(--pui-motion-in);\n  }\n  /* Nesting input (O1 §14.1): the footer (in the shell's lower corners) and the inner panel tell the controls they hold\n     their radius and the padding next to the corner; the control clamps its own concentric radius.\n     --pui-bezel-inner-inset says how far the panel's content is padded (default the card padding). */\n  .footer {\n    --pui-nest-radius: var(--pui-bezel-radius);\n    --pui-nest-inset: calc(var(--pui-gap) + var(--pui-space-1));\n  }\n  .inner {\n    --pui-nest-radius: calc(var(--pui-bezel-radius) - var(--pui-gap) + var(--pui-space-0-5));\n    --pui-nest-inset: var(--pui-bezel-inner-inset, var(--pui-pad-card));\n  }\n  .shell[data-size=\"sm\"] > .footer {\n    --pui-nest-radius: calc(var(--pui-bezel-radius) - var(--pui-gap));\n  }\n  .shell[data-size=\"sm\"] > .inner {\n    --pui-nest-radius: calc(var(--pui-bezel-radius) - 2 * var(--pui-gap) + var(--pui-space-0-5));\n  }\n  .footer {\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-3);\n    min-width: 0;\n    padding: var(--pui-space-0-5) var(--pui-space-1) var(--pui-space-1);\n  }\n  .icon {\n    display: grid;\n    place-items: center;\n    flex: none;\n    width: 40px;\n    height: 40px;\n    /* nested in the shell's corner: shell radius minus the gap and the footer's inline padding */\n    border-radius: max(0px, calc(var(--pui-bezel-radius) - var(--pui-gap) - var(--pui-space-1)));\n    color: var(--pui-ink-2);\n  }\n  .icon > svg {\n    width: 18px;\n    height: 18px;\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.shell {\n  composes: m-surface from global;\n}\n.inner {\n  composes: m-inset from global;\n}\n.icon {\n  composes: m-control from global;\n}\n"},{"path":"beamline/components/bezel/bezel.tsx","type":"registry:file","target":"src/ui/beamline/components/bezel/bezel.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { BezelDefaults } from \"./defaults\";\nexport type { BezelDefaults } from \"./defaults\";\n\nimport { type ComponentProps, type ComponentPropsWithoutRef, type ElementType, type ReactNode, type Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./bezel.module.css\";\n\nexport interface BezelClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the inner part; no override by default. */\n  inner?: string;\n  /** Classes for the footer part; no override by default. */\n  footer?: string;\n}\n\nexport interface BezelProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n  /** Sits in the shell under the panel: an icon tile with a title and a line, or actions. */\n  footer?: ReactNode;\n  /** `sm` for tiles and small previews (6 px gap), `md` for cards (10 px). */\n  size?: \"sm\" | \"md\";\n  /** Extra classes for the inner panel. */\n  innerClassName?: string;\n  /** HTML element used for the text or container. Defaults to \"div\". */\n  as?: \"div\" | \"section\" | \"article\" | \"li\";\n  /** The bezel is something to open (a template, a file, a preview): the picture set into it leans toward the pointer\n   *  (or focus inside) and the frame is a pointer target. Put the link or button inside. */\n  interactive?: boolean;\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?: BezelClassNames;\n}\n\n/**\n * A double-bezel frame: an outer shell with a hairline, a small gap, and an inset panel with its own edge, radii nested\n * so the two curves run parallel. It makes a preview, chart or media read as an object set into the surface. Static\n * CSS paint with scoped presentation defaults. Card, MetricCard and the gallery use it; wrap other content directly.\n */\nexport function Bezel({\n  footer,\n  size: sizeProp,\n  innerClassName,\n  as = \"div\",\n  interactive = false,\n  className,\n  classNames,\n  children,\n  ...props\n}: BezelProps) {\n  const scopeDefaults = useComponentDefaults<BezelDefaults>(\"bezel\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const Tag = as as ElementType;\n  return (\n    <Tag\n      data-slot=\"bezel\"\n      data-size={size}\n      data-interactive={interactive ? \"\" : undefined}\n      className={cn(styles.shell, interactive && \"reveal-host\", classNames?.root, className)}\n      {...props}\n    >\n      <div data-slot=\"bezel-inner\" className={cn(styles.inner, classNames?.inner, innerClassName)}>\n        {children}\n      </div>\n      {footer && (\n        <div data-slot=\"bezel-footer\" className={cn(styles.footer, classNames?.footer)}>\n          {footer}\n        </div>\n      )}\n    </Tag>\n  );\n}\n\n/** An icon tile for a Bezel footer: a small bezel of its own. */\nexport function BezelIcon({ className, ...props }: ComponentProps<\"span\">) {\n  return <span data-slot=\"bezel-icon\" aria-hidden className={cn(styles.icon, className)} {...props} />;\n}\n\nexport default Bezel;\n"},{"path":"beamline/components/bezel/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/bezel/defaults.ts","content":"/** Pure allowed presentation defaults for Bezel; no component or runtime imports. */\nexport interface BezelDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\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/form/form.module.css","type":"registry:file","target":"src/ui/beamline/components/form/form.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A form: fields one under another. Split (layout=\"split\", in a form at least 560px wide): each field's label sits\n     in a column beside its control, so a settings page reads as a list of questions; the control, its help and its\n     problem keep to the second column, and what is not a field (a checkbox, a switch, the problem, the button) lines up\n     with the controls. Narrower, it stacks again. */\n  .form {\n    --pui-form-label-width: 12rem;\n    display: grid;\n    gap: var(--pui-space-4);\n  }\n  .form[data-layout=\"split\"] {\n    --_indent: calc(var(--pui-form-label-width) + var(--pui-space-6));\n    gap: var(--pui-space-5);\n  }\n  .form[data-layout=\"split\"]\n    > :is([data-slot=\"field\"], [data-slot$=\"-field\"], [data-slot=\"radio-group\"]):not(\n      [data-slot=\"checkbox-field\"],\n      [data-slot=\"switch-field\"]\n    ) {\n    display: grid;\n    grid-template-columns: var(--pui-form-label-width) minmax(0, 1fr);\n    column-gap: var(--pui-space-6);\n  }\n  .form[data-layout=\"split\"]\n    > :is([data-slot=\"field\"], [data-slot$=\"-field\"], [data-slot=\"radio-group\"]):not(\n      [data-slot=\"checkbox-field\"],\n      [data-slot=\"switch-field\"]\n    )\n    > * {\n    grid-column: 2;\n  }\n  /* the label takes the first row of its own column, centred on the control's first line (as specific as the rule\n     above that sends every other part to the second column) */\n  .form[data-layout=\"split\"]\n    > :is([data-slot=\"field\"], [data-slot$=\"-field\"], [data-slot=\"radio-group\"]):not(\n      [data-slot=\"checkbox-field\"],\n      [data-slot=\"switch-field\"]\n    )\n    > :is([data-slot=\"label\"], [data-slot=\"field-label-row\"], [data-slot=\"radio-group-label\"]) {\n    grid-row: 1;\n    grid-column: 1;\n    align-self: start;\n    display: flex;\n    min-height: var(--pui-control-height-md);\n    align-items: center;\n  }\n  .form[data-layout=\"split\"] > :is([data-slot=\"checkbox-field\"], [data-slot=\"switch-field\"], .error, button) {\n    margin-inline-start: var(--_indent);\n  }\n\n  /* a failed request: in the problem's colour, with its icon and words, growing to its own height (.enter-grow) */\n  .errorBody {\n    display: flex;\n    align-items: flex-start;\n    gap: var(--pui-space-2);\n    padding: var(--pui-space-2) var(--pui-space-3);\n    border-radius: var(--pui-radius-control);\n    background: var(--pui-red-tint);\n    color: var(--pui-red);\n    font-size: var(--pui-text-sm);\n  }\n  .errorBody :where(svg) {\n    flex-shrink: 0;\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n    margin-top: calc(var(--pui-space-0-5) / 2);\n  }\n}\n"},{"path":"beamline/components/form/form.tsx","type":"registry:file","target":"src/ui/beamline/components/form/form.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useLayoutEffect, useRef, type ComponentProps, type ReactNode } from \"react\";\nimport { AlertCircle } from \"lucide-react\";\nimport {\n  FormProvider,\n  useController,\n  useFormContext,\n  type ControllerRenderProps,\n  type FieldPath,\n  type FieldValues,\n  type RegisterOptions,\n  type UseFormReturn,\n} from \"react-hook-form\";\nimport { Button, type ButtonProps } from \"../button/button\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\nimport { useFlip } from \"../../hooks/use-flip\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./form.module.css\";\n\nexport interface FormProps<T extends FieldValues> extends Omit<ComponentProps<\"form\">, \"onSubmit\"> {\n  /** The result of react-hook-form's useForm (with a zod resolver, or rules per field). */\n  form: UseFormReturn<T>;\n  /** Runs with valid values. Return a promise: FormSubmit shows progress, and a thrown error is shown by FormError. */\n  onSubmit: (values: T) => void | Promise<unknown>;\n  /** stacked (default): labels above the controls. split: in a form at least 560px wide each label (and its required\n   *  mark) sits in a column beside its control, help and problem stay under the control; narrower, it stacks. The\n   *  switch carries every label and control to its new place. */\n  layout?: \"stacked\" | \"split\";\n  /** Content rendered inside the component. Required. */\n  children: ReactNode;\n}\n\n/** A form's room for labels beside their controls (px of the form's own width, not the window's). */\nconst SPLIT_AT = 560;\n/** The parts of a form that are fields laid out as label, control, message (every system field and choice group). */\nconst FIELD = ':scope > :is([data-slot=\"field\"], [data-slot$=\"-field\"], [data-slot=\"radio-group\"])';\n\n/**\n * Validation wiring for react-hook-form on the system's own fields. Every field component already takes a label,\n * description and error and links them for screen readers, so a FormField only hands over the value and the message.\n * The browser's own validation bubbles are off; the first invalid field takes focus on submit.\n */\nexport function Form<T extends FieldValues>({\n  form,\n  onSubmit,\n  layout = \"stacked\",\n  children,\n  className,\n  ref,\n  ...props\n}: FormProps<T>) {\n  const i18n = useMessages(\"form\", englishMessages);\n\n  const root = useRef<HTMLFormElement>(null);\n  const attach = useComposedRef(root, ref);\n  // the form's own width decides (read once per change of the answer, never per frame)\n  const wide = useWidthAs(root, (w) => w >= SPLIT_AT, false);\n  const split = layout === \"split\" && wide;\n  // each field's parts are named for useFlip, so a switch between stacked and split moves every label and control\n  // from where it was instead of jumping (declared before useFlip: the names are on before it reads the places)\n  useLayoutEffect(() => {\n    if (layout !== \"split\" || !root.current) return;\n    root.current.querySelectorAll<HTMLElement>(FIELD).forEach((field, f) => {\n      const name = field.querySelector(\"label[for]\")?.getAttribute(\"for\") ?? `field-${f}`;\n      [...field.children].forEach((part, i) => {\n        // a part that moves its own items (tags, options) keeps them: naming it would hide them from its own useFlip\n        const own = part.hasAttribute(\"data-form-flip\");\n        if (part.querySelector(\"[data-flip]\")) {\n          if (own) {\n            part.removeAttribute(\"data-flip\");\n            part.removeAttribute(\"data-form-flip\");\n          }\n        } else if (!part.hasAttribute(\"data-flip\")) {\n          part.setAttribute(\"data-flip\", `${name}:${i}`);\n          part.setAttribute(\"data-form-flip\", \"\");\n        }\n      });\n    });\n  });\n  useFlip(() => (layout === \"split\" ? root.current : null), { ghost: false, enter: false });\n  return (\n    <FormProvider {...form}>\n      <form\n        ref={attach}\n        noValidate\n        data-slot=\"form\"\n        data-layout={split ? \"split\" : \"stacked\"}\n        className={cn(styles.form, className)}\n        {...props}\n        onSubmit={form.handleSubmit(async (values) => {\n          try {\n            await onSubmit(values);\n          } catch (e) {\n            form.setError(\"root\", { message: e instanceof Error ? e.message : i18n(\"thatDidNotWorkTryAgain\") });\n          }\n        })}\n      >\n        {children}\n      </form>\n    </FormProvider>\n  );\n}\n\nexport interface FormFieldRender<T extends FieldValues, N extends FieldPath<T>> {\n  /** Spread on text-like fields (value, onChange, onBlur, name, ref); map value and onChange by hand for others. */\n  field: ControllerRenderProps<T, N>;\n  /** The message to pass as the field's `error`. */\n  error?: string;\n  /** Whether the field currently has a validation error. Required. */\n  invalid: boolean;\n}\n\nexport interface FormFieldProps<T extends FieldValues, N extends FieldPath<T>> {\n  /** Field path registered with the surrounding React Hook Form. Required. */\n  name: N;\n  /** Rules for this field when there is no schema resolver. */\n  rules?: Omit<RegisterOptions<T, N>, \"valueAsNumber\" | \"valueAsDate\" | \"setValueAs\" | \"disabled\">;\n  /** Renders the current field state and its error. Required. */\n  render: (props: FormFieldRender<T, N>) => ReactNode;\n}\n\n/** The form's state for a part that must sit inside `<Form>`; outside it, a plain sentence instead of react-hook-form's\n *  \"cannot destructure control of null\" (t29: Haiku 5.5's invoice editor crashed on that and lost a turn finding why). */\nfunction useFormOf<T extends FieldValues>(part: string) {\n  const form = useFormContext<T>();\n  if (!form)\n    throw new Error(\n      `${part} must sit inside <Form form={useForm(…)}>: it reads the form's state from there. For a field outside a form, use the field (Input, Select, NumberField…) on its own with value and onValueChange.`,\n    );\n  return form;\n}\n\n/** One field: `render={({ field, error }) => <Input label=\"Email\" error={error} {...field} />}`. */\nexport function FormField<T extends FieldValues, N extends FieldPath<T>>({\n  name,\n  rules,\n  render,\n}: FormFieldProps<T, N>) {\n  const { control } = useFormOf<T>(\"FormField\");\n  const { field, fieldState } = useController<T, N>({ name, control, rules });\n  return <>{render({ field, error: fieldState.error?.message, invalid: fieldState.invalid })}</>;\n}\n\n/** The submit button: shows progress while onSubmit's promise runs. */\nexport function FormSubmit({ children, ...props }: ButtonProps) {\n  const { formState } = useFormOf(\"FormSubmit\");\n  return (\n    <Button type=\"submit\" loading={formState.isSubmitting} {...props}>\n      {children}\n    </Button>\n  );\n}\n\n/** The form-level problem (a failed request), announced when it appears. */\nexport function FormError({ className, ...props }: ComponentProps<\"p\">) {\n  const { formState } = useFormOf(\"FormError\");\n  const message = formState.errors.root?.message;\n  if (!message) return null;\n  return (\n    // it grows to its own height where it appears, so the button below glides down instead of jumping\n    <div className={cn(\"enter-grow\", styles.error)}>\n      <p {...props} role=\"alert\" data-slot=\"form-error\" className={cn(styles.errorBody, className)}>\n        <AlertCircle aria-hidden />\n        {String(message)}\n      </p>\n    </div>\n  );\n}\n\nexport default Form;\n"},{"path":"beamline/components/form/messages.ts","type":"registry:file","target":"src/ui/beamline/components/form/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  thatDidNotWorkTryAgain: \"That did not work. Try again.\",\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/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/marker/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/marker/defaults.ts","content":"/** Pure allowed presentation defaults for Marker; no component or runtime imports. */\nexport interface MarkerDefaults {\n  /** Shared variant setting; omit to inherit the surrounding scope or the component default. */\n  variant?: \"event\" | \"divider\";\n  /** Shared tone setting; omit to inherit the surrounding scope or the component default. */\n  tone?: \"danger\" | \"accent\" | \"success\" | \"warning\" | \"neutral\";\n}\n"},{"path":"beamline/components/marker/marker.module.css","type":"registry:file","target":"src/ui/beamline/components/marker/marker.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* The time stands at the end of the line it falls on, so a stream of markers reads its times as one column, and in\n     a narrow room it simply rides the last line of the words. No containment: a marker can sit in a row and keep its\n     own width. */\n  .root {\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-2);\n    min-width: 0;\n    color: var(--pui-ink-2);\n    font-size: var(--pui-text-sm);\n    line-height: 1.4;\n    list-style: none;\n  }\n  .icon {\n    display: grid;\n    place-items: center;\n    flex: none;\n    width: 16px;\n    height: 16px;\n    color: var(--pui-ink-3);\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .icon > svg {\n    width: 14px;\n    height: 14px;\n  }\n  .label {\n    min-width: 0;\n    flex: 1;\n  }\n  /* the time floats to the end of the line it falls on, so words (and links in them) flow as written */\n  .label :is(time, [data-slot=\"marker-meta\"]) {\n    float: inline-end;\n    margin-inline-start: var(--pui-space-3);\n    color: var(--pui-ink-3);\n    font-variant-numeric: tabular-nums;\n    /* a time keeps its own order inside a right-to-left line (O1 §13 D2) */\n    unicode-bidi: isolate;\n  }\n  .root[data-tone=\"accent\"] .icon {\n    color: var(--pui-accent-ink);\n  }\n  .root[data-tone=\"success\"] .icon {\n    color: var(--pui-green);\n  }\n  .root[data-tone=\"warning\"] .icon {\n    color: var(--pui-orange);\n  }\n  .root[data-tone=\"danger\"] .icon {\n    color: var(--pui-red);\n  }\n  /* Divider: a quiet label centred between two hairlines. */\n  .root[data-variant=\"divider\"] {\n    gap: var(--pui-space-3);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-medium);\n  }\n  .root[data-variant=\"divider\"] .label {\n    flex: none;\n  }\n  .root[data-variant=\"divider\"]::before,\n  .root[data-variant=\"divider\"]::after {\n    content: \"\";\n    flex: 1;\n    height: 1px;\n    background: var(--pui-line-soft);\n  }\n}\n"},{"path":"beamline/components/marker/marker.tsx","type":"registry:file","target":"src/ui/beamline/components/marker/marker.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { MarkerDefaults } from \"./defaults\";\nexport type { MarkerDefaults } from \"./defaults\";\n\nimport { type ComponentProps, type ComponentPropsWithoutRef, type ElementType, type Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./marker.module.css\";\n\nexport type MarkerTone = \"neutral\" | \"accent\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface MarkerProps extends ComponentPropsWithoutRef<\"div\"> {\n  /** `event`: an icon and a line of status in a timeline or thread. `divider`: a label between two hairlines (a day, a phase). */\n  variant?: \"event\" | \"divider\";\n  /** Colours the icon only; the words stay readable ink. */\n  tone?: MarkerTone;\n  /** The element to render: `li` inside a list, `div` elsewhere. */\n  as?: \"div\" | \"li\";\n  ref?: Ref<HTMLElement>;\n}\n\n/**\n * A labelled point in a stream: \"✓ Build passed in 42s\" between messages, or \"Today\" between two days. Compose\n * `Marker` › `MarkerIcon` (optional) › `MarkerLabel`; the label can hold a time or a link.\n */\nexport function Marker({\n  variant: variantProp,\n  tone: toneProp,\n  as = \"div\",\n  className,\n  children,\n  ...props\n}: MarkerProps) {\n  const scopeDefaults = useComponentDefaults<MarkerDefaults>(\"marker\", [\"variant\", \"tone\"]);\n  const variant = variantProp ?? scopeDefaults.variant ?? \"event\";\n  const tone = toneProp ?? scopeDefaults.tone ?? \"neutral\";\n\n  const Tag = as as ElementType;\n  return (\n    <Tag\n      role={variant === \"divider\" && as !== \"li\" ? \"separator\" : undefined}\n      data-slot=\"marker\"\n      data-variant={variant}\n      data-tone={tone}\n      className={cn(styles.root, className)}\n      {...props}\n    >\n      {children}\n    </Tag>\n  );\n}\n\nexport function MarkerIcon({ className, ...props }: ComponentProps<\"span\">) {\n  return <span data-slot=\"marker-icon\" aria-hidden className={cn(styles.icon, className)} {...props} />;\n}\n\nexport function MarkerLabel({ className, ...props }: ComponentProps<\"span\">) {\n  return <span data-slot=\"marker-label\" className={cn(styles.label, className)} {...props} />;\n}\n\nexport default Marker;\n"},{"path":"beamline/components/otp-input/messages.ts","type":"registry:file","target":"src/ui/beamline/components/otp-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  checkingTheCode: \"Checking the code\",\n  characters: \"{label}, {length} characters\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/otp-input/otp-input.module.css","type":"registry:file","target":"src/ui/beamline/components/otp-input/otp-input.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* One real input lies over a row of boxes. Each box wears the look's field role; the box being typed into shows the\n   role's own focus edge, and invalid/disabled change the role's inputs, so metal keeps its pockets and pop its lips. */\n  .root {\n    position: relative;\n    display: flex;\n    width: fit-content;\n    max-width: 100%;\n    align-items: center;\n    gap: var(--pui-space-2);\n  }\n  /* a group gives up width with its boxes, never below its boxes at their smallest, so a box never slides under the\n     separator (--n: the boxes in the group) */\n  .group {\n    display: flex;\n    min-width: calc(var(--n, 1) * var(--pui-space-6) + (var(--n, 1) - 1) * var(--pui-space-1-5));\n    flex: 0 1 auto;\n    gap: var(--pui-space-1-5);\n  }\n  .group:has(> .separator) {\n    min-width: calc(var(--n, 1) * var(--pui-space-6) + var(--n, 1) * var(--pui-space-1-5) + var(--pui-space-3));\n  }\n  /* a box gives up width (and height with it) before the row would spill out of a narrow room; the digit stays legible */\n  .cell {\n    position: relative;\n    display: grid;\n    width: var(--pui-otp-cell, 2.75rem);\n    min-width: var(--pui-space-6);\n    flex: 0 1 var(--pui-otp-cell, 2.75rem);\n    aspect-ratio: 1;\n    place-items: center;\n    border-radius: var(--pui-field-radius, var(--pui-radius-control));\n    color: var(--pui-ink);\n    font-family: var(--pui-font-numeric);\n    font-size: var(--pui-text-xl);\n    font-variant-numeric: tabular-nums;\n    transition: var(--m-transition);\n  }\n  /* the active box's focus edge is drawn by one ring that travels between boxes (GlidePlate); always visible while\n     focused, it only settles its position */\n  .ring {\n    z-index: 2;\n    background: none;\n    border-radius: var(--pui-field-radius, var(--pui-radius-control));\n    box-shadow: var(--m-field-edge-focus);\n  }\n  .root[data-disabled] .cell {\n    opacity: var(--pui-opacity-disabled);\n  }\n  /* where the next digit goes: a steady caret that fades in as each box becomes the active one (no blink loop, so a\n     focused field costs nothing at rest; the travelling ring already says where typing happens) */\n  .caret {\n    position: absolute;\n    width: 1px;\n    height: 45%;\n    background: var(--pui-ink);\n    animation: caret-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  @keyframes caret-in {\n    from {\n      opacity: 0;\n    }\n  }\n  /* checking: the boxes hold still and quiet while the spinner after them works */\n  .root[data-state=\"verifying\"] .cell {\n    color: var(--pui-ink-2);\n  }\n  /* it worked: the edges turn green one box after another, from the first to the last (colour, and the check and the\n     words beside it) */\n  .root[data-state=\"verified\"] .cell {\n    --m-field-ring: var(--pui-green);\n    --m-field-ring-hover: var(--pui-green);\n    transition-delay: calc(var(--i, 0) * var(--pui-stagger-item));\n  }\n  .status {\n    display: inline-grid;\n    flex-shrink: 0;\n    place-items: center;\n    margin-inline-start: var(--pui-space-1);\n    animation: caret-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  .separator {\n    flex-shrink: 0;\n    align-self: center;\n    padding-inline: var(--pui-space-0-5);\n    color: var(--pui-ink-3);\n  }\n  .field {\n    position: absolute;\n    inset: 0;\n    width: 100%;\n    height: 100%;\n    cursor: text;\n    background: transparent;\n    color: transparent;\n    caret-color: transparent;\n    opacity: 0;\n    outline: none;\n  }\n  .field::selection {\n    background: transparent;\n  }\n  .field:disabled {\n    cursor: not-allowed;\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.cell {\n  composes: m-field from global;\n}\n.root {\n  composes: m-field-group from global;\n}\n"},{"path":"beamline/components/otp-input/otp-input.tsx","type":"registry:file","target":"src/ui/beamline/components/otp-input/otp-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 { useEffect, useRef, useState, type CSSProperties, type Ref, type ReactNode } from \"react\";\nimport { Field, useFieldIds } from \"../_shared/field\";\nimport { Spinner } from \"../spinner/spinner\";\nimport type { FieldClassNames } from \"../_shared/field\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { GlidePlate } from \"../_shared/glide\";\nimport styles from \"./otp-input.module.css\";\n\nexport interface OtpInputProps {\n  /** Number of characters. Default 6. */\n  length?: number;\n  /** Controlled verification code; omitted by default to keep the code locally. */\n  value?: string;\n  /** Initial uncontrolled verification code; defaults to an empty string. */\n  defaultValue?: string;\n  /** Legacy value-change callback; omitted by default. Prefer onValueChange.\n   *  @deprecated Use onValueChange; this value callback remains supported and is omitted by default. */\n  onChange?: (value: string) => void;\n  /** Reports the cleaned verification code after typing or pasting; no callback by default. */\n  onValueChange?: (value: string) => void;\n  /** Runs once every character is in. */\n  onComplete?: (value: string) => void;\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  /** The problem and its fix (\"That code did not match\"): the boxes turn red and the whole code is selected, so the\n   *  next digit typed starts again. */\n  error?: ReactNode;\n  /** The code is being checked: the boxes hold still and a spinner sits after them (aria-busy). */\n  loading?: boolean;\n  /** The code worked (\"Verified\"): the spinner closes into a check and the boxes' edges turn green one after another. */\n  success?: ReactNode;\n  /** Moves focus to the field after it mounts. Defaults to false. */\n  autoFocus?: boolean;\n  /** Prevents interaction with the control. Defaults to false. */\n  disabled?: boolean;\n  /** numeric (default) shows the number pad on phones; text allows letters. */\n  inputMode?: \"numeric\" | \"text\";\n  /** Separate the boxes into groups, e.g. [3, 3] for \"123 456\". */\n  groups?: number[];\n  /** DOM id used to connect the control and its label. Defaults to an automatically generated control id. */\n  id?: string;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: Pick<FieldClassNames, \"root\" | \"label\" | \"description\" | \"error\"> &\n    Partial<Record<\"frame\" | \"group\" | \"cell\" | \"caret\" | \"separator\" | \"input\", string>>;\n  /** Receives the native input; no ref by default. */\n  ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * A verification code in separate boxes, backed by one real field: paste, SMS autofill (one-time-code), Backspace and\n * arrow keys all behave as in any text field. The box being typed into shows a caret.\n */\nexport function OtpInput({\n  length = 6,\n  value: valueProp,\n  defaultValue = \"\",\n  onChange,\n  onValueChange,\n  onComplete,\n  label,\n  hideLabel: hideLabelProp,\n  showLabel,\n  description,\n  error,\n  loading,\n  success,\n  autoFocus,\n  disabled,\n  inputMode = \"numeric\",\n  groups,\n  id,\n  className,\n  classNames,\n  style,\n  ref,\n}: OtpInputProps) {\n  const i18n = useMessages(\"otp-input\", englishMessages);\n\n  useDeprecatedProp(\"OtpInput\", \"hideLabel\", \"showLabel={false}\", hideLabelProp !== undefined);\n  const hideLabel = showLabel === undefined ? hideLabelProp : !showLabel;\n  useDeprecatedProp(\"OtpInput\", \"onChange\", \"onValueChange\", onChange !== undefined);\n  const [value, setValue] = useControllable(valueProp, defaultValue, onChange);\n  const input = useRef<HTMLInputElement>(null);\n  const attach = useComposedRef(input, ref);\n  const [focused, setFocused] = useState(false);\n  const row = useRef<HTMLDivElement>(null);\n  const [caret, setCaret] = useState(0);\n  const { controlId, descriptionId, errorId, aria } = useFieldIds(id, { description, error, success });\n  const state = error ? \"invalid\" : success ? \"verified\" : loading ? \"verifying\" : undefined;\n  const clean = (s: string) =>\n    (inputMode === \"numeric\" ? s.replace(/\\D/g, \"\") : s.replace(/\\s/g, \"\").toUpperCase()).slice(0, length);\n  useEffect(() => {\n    if (autoFocus) input.current?.focus();\n  }, [autoFocus]);\n  // a wrong code is selected whole, so the next digit typed starts again instead of meeting a full field\n  useEffect(() => {\n    const el = input.current;\n    if (!error || !el || el.value.length < length) return;\n    el.setSelectionRange(0, el.value.length);\n    requestAnimationFrame(() => setCaret(0));\n  }, [error, length]);\n  const sync = () => {\n    const el = input.current;\n    if (el) setCaret(Math.min(el.selectionStart ?? el.value.length, length - 1));\n  };\n  const split = groups ?? [length];\n  let n = 0;\n  return (\n    <Field\n      label={label}\n      hideLabel={hideLabel}\n      description={description}\n      error={error}\n      success={error ? undefined : success}\n      controlId={controlId}\n      descriptionId={descriptionId}\n      errorId={errorId}\n      className={className}\n      classNames={classNames}\n      style={style}\n    >\n      <div\n        ref={row}\n        data-slot=\"otp-input\"\n        className={cn(styles.root, classNames?.frame)}\n        data-invalid={error ? \"\" : undefined}\n        data-state={state}\n        data-disabled={disabled ? \"\" : undefined}\n      >\n        {/* the focus edge is one ring that travels from box to box as the code is typed */}\n        <GlidePlate\n          list={row}\n          selector={focused && !loading && !success ? \"[data-slot=otp-input-cell][data-active]\" : null}\n          axis=\"xy\"\n          className={styles.ring}\n        />\n        {split.map((size, g) => (\n          <div\n            key={g}\n            data-slot=\"otp-input-group\"\n            className={cn(styles.group, classNames?.group)}\n            style={{ \"--n\": size } as CSSProperties}\n          >\n            {Array.from({ length: size }, () => {\n              const i = n++;\n              const char = value[i];\n              const here =\n                focused && (i === caret || (i === length - 1 && value.length === length && caret >= length - 1));\n              return (\n                <div\n                  key={i}\n                  aria-hidden\n                  data-slot=\"otp-input-cell\"\n                  data-active={here ? \"\" : undefined}\n                  data-filled={char ? \"\" : undefined}\n                  className={cn(styles.cell, classNames?.cell)}\n                  style={{ \"--i\": i } as CSSProperties}\n                >\n                  {char ?? \"\"}\n                  {here && !char && (\n                    <span data-slot=\"otp-input-caret\" className={cn(styles.caret, classNames?.caret)} />\n                  )}\n                </div>\n              );\n            })}\n            {g < split.length - 1 && (\n              <span data-slot=\"otp-input-separator\" aria-hidden className={cn(styles.separator, classNames?.separator)}>\n                –\n              </span>\n            )}\n          </div>\n        ))}\n        {(loading || success) && !error && (\n          // the same ring every field uses: it spins while the code is checked and closes into a check when it works\n          <span data-slot=\"otp-input-status\" className={styles.status}>\n            <Spinner size=\"sm\" state={loading ? \"loading\" : \"success\"} label={i18n(\"checkingTheCode\")} />\n          </span>\n        )}\n        <input\n          ref={attach}\n          {...aria}\n          aria-busy={loading || undefined}\n          data-slot=\"otp-input-field\"\n          value={value}\n          disabled={disabled}\n          readOnly={loading}\n          inputMode={inputMode}\n          autoComplete=\"one-time-code\"\n          maxLength={length}\n          spellCheck={false}\n          pattern={inputMode === \"numeric\" ? `\\\\d{${length}}` : undefined}\n          aria-label={typeof label === \"string\" ? i18n(\"characters\", { label: label, length: length }) : undefined}\n          onChange={(e) => {\n            const v = clean(e.target.value);\n            setValue(v);\n            onValueChange?.(v);\n            requestAnimationFrame(sync);\n            if (v.length === length && value.length !== length) onComplete?.(v);\n          }}\n          onFocus={() => {\n            setFocused(true);\n            requestAnimationFrame(sync);\n          }}\n          onBlur={() => setFocused(false)}\n          onSelect={sync}\n          onKeyUp={sync}\n          onClick={sync}\n          className={cn(styles.field, classNames?.input)}\n        />\n      </div>\n    </Field>\n  );\n}\n\nexport default OtpInput;\n"},{"path":"beamline/components/password-field/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/password-field/defaults.ts","content":"/** Pure allowed presentation defaults for PasswordField; no component or runtime imports. */\nexport interface PasswordFieldDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/password-field/messages.ts","type":"registry:file","target":"src/ui/beamline/components/password-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  hidePassword: \"Hide password\",\n  showPassword: \"Show password\",\n  capsLock: \"Caps Lock\",\n  capsLockIsOn: \"Caps Lock is on\",\n  ofRulesMet: \"{metCount} of {length} rules met\",\n  passwordRules: \"Password rules\",\n  met: \", met\",\n  notMetYet: \", not met yet\",\n  weak: \"Weak\",\n  fair: \"Fair\",\n  good: \"Good\",\n  strong: \"Strong\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/password-field/password-field.module.css","type":"registry:file","target":"src/ui/beamline/components/password-field/password-field.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Password field: the reveal toggle and the Caps Lock notice at the end of the frame; for a new password, a strength\n     meter whose segments fill from the start and drain back, and the rules it must meet, each ticked as it is met. */\n  .reveal {\n    display: grid;\n    width: var(--pui-space-7);\n    height: var(--pui-space-7);\n    margin-inline-end: calc(var(--pui-space-1) * -1);\n    place-items: center;\n    border-radius: var(--pui-radius-chip);\n    color: var(--pui-ink-3);\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .reveal:hover:not(:disabled) {\n    background: var(--pui-hover);\n    color: var(--pui-ink);\n    transition-duration: 0ms;\n  }\n  .reveal:active:not(:disabled) {\n    scale: var(--pui-press-scale);\n  }\n\n  /* said in words beside the cause, in the warning colour, never colour alone (the arrow and the word) */\n  .caps {\n    display: inline-flex;\n    align-items: center;\n    gap: var(--pui-space-1);\n    padding-inline: var(--pui-space-1-5);\n    height: var(--pui-space-5);\n    border-radius: var(--pui-radius-chip);\n    background: color-mix(in oklab, var(--pui-orange) 14%, transparent);\n    color: var(--pui-orange);\n    font-size: var(--pui-text-2xs);\n    font-weight: var(--pui-weight-medium);\n    white-space: nowrap;\n    animation: caps-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  .caps :where(svg) {\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n  }\n  @keyframes caps-in {\n    from {\n      opacity: 0;\n      scale: calc(1 - 0.15 * var(--pui-motion-travel, 1));\n    }\n  }\n\n  .strength {\n    position: relative;\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-2);\n  }\n  .strength[data-score=\"1\"] {\n    --_tone: var(--pui-red);\n  }\n  .strength[data-score=\"2\"] {\n    --_tone: var(--pui-orange);\n  }\n  .strength[data-score=\"3\"] {\n    --_tone: var(--pui-blue);\n  }\n  .strength[data-score=\"4\"] {\n    --_tone: var(--pui-green);\n  }\n  .meter {\n    display: grid;\n    flex: 1;\n    grid-template-columns: repeat(4, 1fr);\n    gap: var(--pui-space-1);\n  }\n  .segment {\n    height: var(--pui-space-1);\n    overflow: hidden;\n    border-radius: var(--pui-radius-pill);\n    background: var(--pui-hover-2);\n  }\n  /* each segment fills from its start edge, one after another as the score rises, and drains together as it falls */\n  .segment::after {\n    content: \"\";\n    display: block;\n    height: 100%;\n    background: var(--_tone, transparent);\n    transform: scaleX(0);\n    transform-origin: left;\n    transition:\n      transform var(--pui-duration-standard) var(--pui-ease-default),\n      background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .segment:dir(rtl)::after {\n    transform-origin: right;\n  }\n  .segment[data-on]::after {\n    transform: none;\n    transition-delay: calc(var(--i, 0) * var(--pui-stagger-item)), 0s;\n  }\n  .word {\n    min-inline-size: 7ch;\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    text-align: end;\n  }\n  .wordIn {\n    display: inline-block;\n    animation: word-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  @keyframes word-in {\n    from {\n      opacity: 0;\n      translate: 0 calc(3px * var(--pui-motion-travel, 1));\n    }\n  }\n\n  /* the rules carry their own gap inside the fold, so a closed list takes no room under the field */\n  .rules {\n    margin-top: calc(var(--pui-field-gap, var(--pui-space-1-5)) * -1);\n  }\n  .ruleList {\n    display: grid;\n    gap: var(--pui-space-1);\n    padding-top: var(--pui-field-gap, var(--pui-space-1-5));\n  }\n  .rule {\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-1-5);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .rule[data-met] {\n    color: var(--pui-ink-2);\n    transition-duration: var(--pui-motion-in);\n  }\n  .tick {\n    flex-shrink: 0;\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n    fill: none;\n    stroke: currentColor;\n    stroke-width: 2;\n    stroke-linecap: round;\n    stroke-linejoin: round;\n  }\n  /* unmet: a quiet dot; met: the dot gives way to a green check that draws itself */\n  .dot {\n    fill: currentColor;\n    stroke: none;\n    transition: opacity var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .check {\n    color: var(--pui-green);\n    stroke: var(--pui-green);\n    stroke-dasharray: 14;\n    stroke-dashoffset: 14;\n    transition: stroke-dashoffset var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .rule[data-met] .dot {\n    opacity: 0;\n  }\n  .rule[data-met] .check {\n    stroke-dashoffset: 0;\n  }\n}\n"},{"path":"beamline/components/password-field/password-field.tsx","type":"registry:file","target":"src/ui/beamline/components/password-field/password-field.tsx","content":"\"use client\";\n\nimport { type Translator } from \"../../runtime/messages\";\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 { PasswordFieldDefaults } from \"./defaults\";\nimport { cssEase } from \"../../lib/motion-tokens\";\nexport type { PasswordFieldDefaults } from \"./defaults\";\n\nimport {\n  useLayoutEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type InputHTMLAttributes,\n  type KeyboardEvent,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { ArrowBigUp, Eye, EyeOff } from \"lucide-react\";\nimport {\n  Field,\n  FieldFrame,\n  fieldControlClass,\n  useFieldIds,\n  type FieldClassNames,\n  type FieldSize,\n} from \"../_shared/field\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport styles from \"./password-field.module.css\";\n\nexport type PasswordStrength = 0 | 1 | 2 | 3 | 4;\n\n/** One rule a new password must meet, checked as it is typed (\"At least 12 characters\"). */\nexport interface PasswordRequirement {\n  /** Text displayed for this item. Required. */\n  label: string;\n  /** Returns true when the password meets this rule. Required. */\n  test: (value: string) => boolean;\n}\n\nexport interface PasswordFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\" | \"size\"> {\n  /** Visible label or accessible name for the control. Required. */\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  /** 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  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: FieldSize;\n  /** Show a strength meter (for creating a password, not for signing in). */\n  showStrength?: boolean;\n  /** Your own scorer (0 = empty … 4 = strong); the built-in one weighs length, variety and obvious patterns. */\n  scoreStrength?: (value: string) => PasswordStrength;\n  /** Rules for a new password, ticked off as they are met. The list opens while the field has attention and stays\n   *  open while one is unmet; when every rule is met it folds away. */\n  requirements?: PasswordRequirement[];\n  /** At the end of the label's row: \"Forgot password?\". */\n  aside?: ReactNode;\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 &\n    Partial<Record<\"input\" | \"reveal\" | \"strength\" | \"requirements\" | \"requirement\" | \"caps\", string>>;\n  /** Receives the native input; no ref by default. */\n  ref?: Ref<HTMLInputElement>;\n}\n\nconst COMMON = /^(password|passwort|qwerty|letmein|welcome|admin|iloveyou|123456|abc123)/i;\n/** Length first, then variety; repeated runs, sequences and well-known passwords are marked down. */\nexport function scorePassword(v: string): PasswordStrength {\n  if (!v) return 0;\n  let s = v.length >= 16 ? 3 : v.length >= 12 ? 2 : v.length >= 8 ? 1 : 0;\n  const kinds = [/[a-z]/, /[A-Z]/, /\\d/, /[^\\w\\s]/].filter((r) => r.test(v)).length;\n  if (kinds >= 3) s += 1;\n  if (/(.)\\1{2,}/.test(v) || /(0123|1234|2345|3456|4567|5678|6789|abcd|bcde|cdef)/i.test(v)) s -= 1;\n  if (COMMON.test(v)) s = Math.min(s, 1);\n  return Math.max(1, Math.min(4, s)) as PasswordStrength;\n}\nfunction localizedWORDS(i18n: Translator<typeof englishMessages>) {\n  return [\"\", i18n(\"weak\"), i18n(\"fair\"), i18n(\"good\"), i18n(\"strong\")] as const;\n}\n\n/**\n * A password field with a reveal control and a Caps Lock notice; for a new password, a strength meter and the rules it\n * must meet, ticked off as it is typed.\n */\nexport function PasswordField({\n  label,\n  hideLabel: hideLabelProp,\n  showLabel,\n  description,\n  error,\n  size: sizeProp,\n  showStrength = false,\n  scoreStrength = scorePassword,\n  requirements,\n  aside,\n  id,\n  className,\n  classNames,\n  style,\n  required,\n  disabled,\n  value,\n  defaultValue,\n  onChange,\n  onValueChange,\n  onKeyDown,\n  onKeyUp,\n  onFocus,\n  onBlur,\n  ref,\n  ...props\n}: PasswordFieldProps) {\n  const i18n = useMessages(\"password-field\", englishMessages);\n\n  useDeprecatedProp(\"PasswordField\", \"hideLabel\", \"showLabel={false}\", hideLabelProp !== undefined);\n  const hideLabel = showLabel === undefined ? hideLabelProp : !showLabel;\n  const scopeDefaults = useComponentDefaults<PasswordFieldDefaults>(\"password-field\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const { controlId, descriptionId, errorId, aria } = useFieldIds(id, { description, error });\n  const [visible, setVisible] = useState(false);\n  const [caps, setCaps] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const field = useRef<HTMLInputElement>(null);\n  const attach = useComposedRef(field, ref);\n  const motion = useMotionTokens(field);\n  // the native change event is passed on as it is, so the hook only holds the text\n  const [text, setText] = useControllable(value !== undefined ? String(value) : undefined, String(defaultValue ?? \"\"));\n  const score = showStrength ? scoreStrength(text) : 0;\n  const met = requirements?.map((r) => r.test(text)) ?? [];\n  const metCount = met.filter(Boolean).length;\n  // the rules wait for attention, stay while one is unmet, and fold away once every one is met\n  const rulesOpen = !!requirements?.length && (focused || (text !== \"\" && metCount < met.length));\n  const readCaps = (e: KeyboardEvent<HTMLInputElement>) => setCaps(e.getModifierState?.(\"CapsLock\") ?? false);\n  // a caller that already says it (an auth form's own hint under the field) is not repeated at the end of the frame\n  const [saidByCaller, setSaidByCaller] = useState(false);\n  useLayoutEffect(() => {\n    const said = !!field.current?.closest(\"[data-slot=field]\")?.querySelector(\"[data-slot=caps-lock-hint]\");\n    setSaidByCaller((s) => (s === said ? s : said));\n  }, [caps, focused]);\n  const showCaps = caps && focused && !saidByCaller;\n  const reveal = (\n    <button\n      type=\"button\"\n      data-slot=\"password-reveal\"\n      disabled={disabled}\n      aria-label={visible ? i18n(\"hidePassword\") : i18n(\"showPassword\")}\n      aria-pressed={visible}\n      onClick={() => {\n        setVisible((v) => !v);\n        // dots and letters swap through a short blur, so the change reads as one text turning, not a jump\n        if (!motion.reduced) {\n          const ease = motion.ease.standard;\n          field.current?.animate(\n            [\n              { filter: \"blur(3px)\", opacity: 0.4 },\n              { filter: \"none\", opacity: 1 },\n            ],\n            {\n              duration: motion.duration.standard * 1000,\n              easing: cssEase(ease, undefined),\n            },\n          );\n        }\n      }}\n      className={cn(styles.reveal, classNames?.reveal)}\n    >\n      {visible ? (\n        <EyeOff key=\"off\" className=\"size-4 animate-enter-scale\" />\n      ) : (\n        <Eye key=\"on\" className=\"size-4 animate-enter-scale\" />\n      )}\n    </button>\n  );\n  return (\n    <Field\n      label={label}\n      hideLabel={hideLabel}\n      description={description}\n      error={error}\n      aside={aside}\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      <FieldFrame\n        classNames={classNames}\n        size={size}\n        invalid={!!error}\n        disabled={disabled}\n        trailing={\n          <>\n            {showCaps && (\n              // said where the typing happens, the moment it matters: a password typed in capitals fails silently\n              <span data-slot=\"password-caps\" className={cn(styles.caps, classNames?.caps)}>\n                <ArrowBigUp aria-hidden />\n                <span className={styles.capsWord}>{i18n(\"capsLock\")}</span>\n              </span>\n            )}\n            {reveal}\n          </>\n        }\n      >\n        <input\n          ref={attach}\n          data-slot=\"password-field\"\n          {...aria}\n          type={visible ? \"text\" : \"password\"}\n          required={required}\n          disabled={disabled}\n          value={value}\n          defaultValue={defaultValue}\n          autoCapitalize=\"none\"\n          autoCorrect=\"off\"\n          spellCheck={false}\n          className={cn(fieldControlClass, classNames?.input)}\n          onChange={(e) => {\n            setText(e.target.value);\n            onChange?.(e);\n            onValueChange?.(e.target.value);\n          }}\n          onKeyDown={(e) => {\n            readCaps(e);\n            onKeyDown?.(e);\n          }}\n          onKeyUp={(e) => {\n            readCaps(e);\n            onKeyUp?.(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        {/* live words stay inside the frame (it is positioned), heard when they change, never at every key */}\n        <span className=\"sr-only\" aria-live=\"polite\">\n          {showCaps ? i18n(\"capsLockIsOn\") : \"\"}\n        </span>\n        {!!requirements?.length && (\n          <span className=\"sr-only\" aria-live=\"polite\">\n            {text ? i18n(\"ofRulesMet\", { metCount: metCount, length: met.length }) : \"\"}\n          </span>\n        )}\n      </FieldFrame>\n      {showStrength && (\n        <div data-slot=\"password-strength\" data-score={score} className={cn(styles.strength, classNames?.strength)}>\n          <div className={styles.meter} aria-hidden>\n            {[1, 2, 3, 4].map((i) => (\n              <span\n                key={i}\n                data-on={score >= i ? \"\" : undefined}\n                className={styles.segment}\n                style={{ \"--i\": i - 1 } as CSSProperties}\n              />\n            ))}\n          </div>\n          {/* a new word rises in where the old one was; it is also what assistive tech hears, once per change */}\n          <span className={styles.word} aria-live=\"polite\">\n            {text ? (\n              <span key={score} className={styles.wordIn}>\n                {localizedWORDS(i18n)[score]}\n              </span>\n            ) : (\n              \"\"\n            )}\n          </span>\n        </div>\n      )}\n      {!!requirements?.length && (\n        <div\n          data-slot=\"password-requirements\"\n          data-open={rulesOpen ? \"\" : undefined}\n          className={cn(\"disclosure-panel\", styles.rules, classNames?.requirements)}\n        >\n          <div>\n            <ul className={styles.ruleList} aria-label={i18n(\"passwordRules\")}>\n              {requirements.map((r, i) => (\n                <li\n                  key={r.label}\n                  data-slot=\"password-requirement\"\n                  data-met={met[i] ? \"\" : undefined}\n                  className={cn(styles.rule, classNames?.requirement)}\n                >\n                  <svg viewBox=\"0 0 12 12\" aria-hidden className={styles.tick}>\n                    <circle className={styles.dot} cx=\"6\" cy=\"6\" r=\"1.5\" />\n                    <path className={styles.check} d=\"M2.5 6.2 5 8.6l4.6-5\" />\n                  </svg>\n                  {r.label}\n                  <span className=\"sr-only\">{met[i] ? i18n(\"met\") : i18n(\"notMetYet\")}</span>\n                </li>\n              ))}\n            </ul>\n          </div>\n        </div>\n      )}\n    </Field>\n  );\n}\n\nexport default PasswordField;\n"},{"path":"beamline/components/spinner/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/spinner/defaults.ts","content":"/** Pure allowed presentation defaults for Spinner; no component or runtime imports. */\nexport interface SpinnerDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/spinner/messages.ts","type":"registry:file","target":"src/ui/beamline/components/spinner/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  label: \"Loading\",\n  done: \"Done\",\n  error: \"Failed\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/spinner/spinner.module.css","type":"registry:file","target":"src/ui/beamline/components/spinner/spinner.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* With a delay the ring waits unseen and then fades in, so quick work never flashes it; the result (a check or a\n     cross) always shows at once. */\n  .root[data-delay] {\n    animation: appear var(--pui-fade-in) var(--pui-ease-default) var(--_delay) both;\n  }\n  @keyframes appear {\n    from {\n      opacity: 0;\n    }\n  }\n  /* The turn runs on the compositor (a CSS animation on the svg box) and costs no JS per frame. When the work ends the\n     arc closes into a full ring over the standard duration, so stopping the turn is invisible, and the check or cross\n     draws itself a beat later. All timing comes from the motion tokens; the loop period follows --pui-motion-scale. */\n  .svg[data-state=\"loading\"] {\n    animation: turn calc(0.8s * var(--pui-motion-scale, 1) * var(--pui-motion-look, 1)) linear infinite;\n  }\n  .track {\n    stroke-opacity: 0.18;\n  }\n  .arc {\n    stroke-dasharray: 0.3 1;\n    transition: stroke-dasharray var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .svg:not([data-state=\"loading\"]) .arc {\n    stroke-dasharray: 1 1;\n  }\n  .mark {\n    stroke-dasharray: 1 1;\n    animation: draw var(--pui-duration-standard) var(--pui-ease-enter) var(--pui-duration-fast) both;\n  }\n  @keyframes turn {\n    to {\n      transform: rotate(1turn);\n    }\n  }\n  @keyframes draw {\n    from {\n      stroke-dashoffset: 1;\n    }\n    to {\n      stroke-dashoffset: 0;\n    }\n  }\n  .svg[data-paused] {\n    animation-play-state: paused;\n  }\n  .svg[data-reduced],\n  .svg[data-reduced] .mark {\n    animation: none;\n  }\n  /* Reduction retains the loading/result shape without a decorative loop. */\n  @media (prefers-reduced-motion: reduce) {\n    .svg[data-state=\"loading\"],\n    .mark {\n      animation: none;\n    }\n  }\n}\n"},{"path":"beamline/components/spinner/spinner.tsx","type":"registry:file","target":"src/ui/beamline/components/spinner/spinner.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SpinnerDefaults } from \"./defaults\";\nexport type { SpinnerDefaults } from \"./defaults\";\n\nimport { useRef, type CSSProperties, type Ref } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./spinner.module.css\";\n\nexport interface SpinnerClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the svg part; no override by default. */\n  svg?: string;\n  /** Classes for the track part; no override by default. */\n  track?: string;\n  /** Classes for the arc part; no override by default. */\n  arc?: string;\n  /** Classes for the mark part; no override by default. */\n  mark?: string;\n}\n\nexport interface SpinnerProps {\n  /** `success` draws a check out of the ring, `error` a cross; both stop spinning. */\n  state?: \"loading\" | \"success\" | \"error\";\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Accessible name while loading. */\n  label?: string;\n  /** ms to wait before it shows: work that ends sooner never flashes a spinner (about 300 is a good start). Default 0. */\n  delay?: number;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLSpanElement>;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SpinnerClassNames;\n}\n\n/** Icon box from the foundation icon steps (§14.2): sm 14, md 16, lg 20. */\nconst PX = { sm: \"var(--pui-icon-sm)\", md: \"var(--pui-icon-md)\", lg: \"var(--pui-icon-lg)\" } as const;\n\n/**\n * A ring that spins while work runs and closes into a drawn check (or cross) when it ends — the result is a shape as\n * well as a colour. With reduced motion the loading ring holds still.\n */\nexport function Spinner({\n  state = \"loading\",\n  size: sizeProp,\n  label: labelProp,\n  delay = 0,\n  className,\n  classNames,\n  style,\n  ref,\n}: SpinnerProps) {\n  const i18n = useMessages(\"spinner\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n\n  const scopeDefaults = useComponentDefaults<SpinnerDefaults>(\"spinner\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = tokens.reduced || managedReduced;\n\n  const box = PX[size];\n  const done = state !== \"loading\";\n  const tone = state === \"success\" ? \"text-green\" : state === \"error\" ? \"text-red\" : \"text-ink-2\";\n  return (\n    <span\n      ref={attach}\n      data-slot=\"spinner\"\n      data-state={state}\n      data-delay={delay > 0 && state === \"loading\" ? \"\" : undefined}\n      role={done ? \"img\" : \"status\"}\n      aria-label={state === \"success\" ? i18n(\"done\") : state === \"error\" ? i18n(\"error\") : label}\n      className={cn(styles.root, \"relative inline-grid shrink-0 place-items-center\", tone, classNames?.root, className)}\n      style={{ width: box, height: box, \"--_delay\": `${delay}ms`, ...style } as CSSProperties}\n    >\n      <svg\n        viewBox=\"0 0 24 24\"\n        width=\"100%\"\n        height=\"100%\"\n        fill=\"none\"\n        aria-hidden=\"true\"\n        data-state={state}\n        data-slot=\"spinner-svg\"\n        data-reduced={reduced ? \"\" : undefined}\n        data-paused={active ? undefined : \"\"}\n        className={cn(styles.svg, classNames?.svg)}\n      >\n        <circle\n          data-slot=\"spinner-track\"\n          className={cn(styles.track, classNames?.track)}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"9.5\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n        />\n        <circle\n          data-slot=\"spinner-arc\"\n          className={cn(styles.arc, classNames?.arc)}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"9.5\"\n          pathLength={1}\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n          strokeLinecap=\"round\"\n          transform=\"rotate(-90 12 12)\"\n        />\n        {state === \"success\" && (\n          <path\n            data-slot=\"spinner-mark\"\n            className={cn(styles.mark, classNames?.mark)}\n            d=\"M7.5 12.5l3 3 6-6.5\"\n            pathLength={1}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        )}\n        {state === \"error\" && (\n          <path\n            data-slot=\"spinner-mark\"\n            className={cn(styles.mark, classNames?.mark)}\n            d=\"M8.5 8.5l7 7M15.5 8.5l-7 7\"\n            pathLength={1}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.5\"\n            strokeLinecap=\"round\"\n          />\n        )}\n      </svg>\n    </span>\n  );\n}\n\nexport default Spinner;\n"},{"path":"beamline/hooks/use-active.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-active.ts","content":"\"use client\";\n\nimport { useContext, useEffect, useState, type RefObject } from \"react\";\n\nimport { ActivityContext } from \"../runtime/activity\";\n\n/**\n * Whether an animated element should be doing work: it is (nearly) on screen and the tab is visible. Every\n * continuously animated component gates its loop, shader or CSS animation on this, so an effect scrolled out of view,\n * behind another tab, or in a collapsed panel costs nothing. One IntersectionObserver per element; no polling.\n */\nexport function useActive(\n  ref: RefObject<Element | null>,\n  {\n    margin = \"120px\",\n    key,\n    idle = true,\n  }: {\n    margin?: string;\n    /** Re-attach when this changes (an element that mounts later). */\n    key?: unknown;\n    /** Pause continuous work at the host idle boundary. Finite sequences set false so they finish. */\n    idle?: boolean;\n  } = {},\n) {\n  const active = useContext(ActivityContext);\n  const [inView, setInView] = useState(false);\n  const [visible, setVisible] = useState(\n    () => typeof document === \"undefined\" || document.visibilityState === \"visible\",\n  );\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") {\n      setInView(true);\n      return;\n    }\n    const io = new IntersectionObserver(([e]) => setInView(e.isIntersecting), { rootMargin: margin });\n    io.observe(el);\n    return () => io.disconnect();\n  }, [ref, margin, key]);\n  useEffect(() => {\n    const on = () => setVisible(document.visibilityState === \"visible\");\n    document.addEventListener(\"visibilitychange\", on);\n    return () => document.removeEventListener(\"visibilitychange\", on);\n  }, []);\n  return (!idle || active) && inView && visible;\n}\n"},{"path":"beamline/hooks/use-composed-ref.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-composed-ref.ts","content":"\"use client\";\n\nimport { useCallback, type Ref, type RefObject } from \"react\";\n\n/** Keep a control's measurement/focus ref and the caller's ref, including React 19 callback cleanup. */\nexport function useComposedRef<T>(internal: RefObject<T | null>, external?: Ref<T>) {\n  return useCallback(\n    (node: T | null) => {\n      internal.current = node;\n      if (typeof external === \"function\") {\n        const cleanup = external(node);\n        if (typeof cleanup === \"function\")\n          return () => {\n            internal.current = null;\n            cleanup();\n          };\n      } else if (external) external.current = node;\n    },\n    [internal, external],\n  );\n}\n"},{"path":"beamline/hooks/use-content-width.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-content-width.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\nconst same = (width: number) => width;\n\n/**\n * The width of an element's content box, kept current with a ResizeObserver. It is first read in a layout effect, so\n * a layout chosen by width (a narrow mode, a chart's scale) is right in the first frame instead of jumping a frame\n * later. `fallback` stands in on the server and until the element exists; `key` re-measures when the element is\n * replaced (one that exists only in one view).\n */\nexport function useContentWidth(ref: RefObject<HTMLElement | null>, fallback: number, key?: unknown): number {\n  return useWidthAs(ref, same, fallback, key);\n}\n\n/**\n * The same width, kept as what a part needs from it (`(w) => w < 640` for a narrow mode): the part renders again only\n * when that changes, not at every pixel.\n */\nexport function useWidthAs<T>(\n  ref: RefObject<HTMLElement | null>,\n  read: (width: number) => T,\n  fallback: T,\n  key?: unknown,\n): T {\n  const [value, setValue] = useState(fallback);\n  const latest = useRef(read);\n  latest.current = read;\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    // the computed width is fractional and ignores transforms, like the observer's content box\n    const s = getComputedStyle(el);\n    const edges = [\"paddingLeft\", \"paddingRight\", \"borderLeftWidth\", \"borderRightWidth\"] as const;\n    const box = parseFloat(s.width);\n    const outer = Number.isFinite(box) ? box : el.offsetWidth;\n    const inner =\n      Number.isFinite(box) && s.boxSizing !== \"border-box\" ? 0 : edges.reduce((w, k) => w + parseFloat(s[k]), 0);\n    setValue(latest.current(outer - inner));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(latest.current(e.contentRect.width)));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key]);\n  return value;\n}\n\n/**\n * The height of an element's content box, kept current like `useContentWidth`: for parts that lay out at whatever\n * height their parent gives them (a chart, a map or a book that fills its panel). Read in a layout effect, so the first\n * frame is right; with `enabled` false nothing is measured and the fallback stands.\n */\nexport function useContentHeight(\n  ref: RefObject<HTMLElement | null>,\n  fallback: number,\n  key?: unknown,\n  enabled = true,\n): number {\n  const [value, setValue] = useState(fallback);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!enabled || !el) return;\n    const s = getComputedStyle(el);\n    setValue(el.clientHeight - (parseFloat(s.paddingTop) || 0) - (parseFloat(s.paddingBottom) || 0));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(e.contentRect.height));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key, enabled]);\n  return enabled ? value : fallback;\n}\n"},{"path":"beamline/hooks/use-controllable.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-controllable.ts","content":"\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\n/**\n * One value in both forms (rules/CONVENTIONS.md): the prop while it is passed, otherwise the element's own copy that\n * starts at `initial` (a value, or a function that makes it once). The setter takes a value or an updater; an updater\n * reads the latest value even twice in one event, and every change reaches `onChange`, controlled or not.\n * `Next` is what a change can carry when it is narrower than the state: a range that starts empty (null) but is only\n * ever changed to a range.\n */\nexport function useControllable<T, Next extends T = T>(\n  value: T | undefined,\n  initial: T | (() => T),\n  onChange?: (next: Next) => void,\n) {\n  const [own, setOwn] = useState<T>(initial);\n  const current = value !== undefined ? value : own;\n  const latest = useRef(current);\n  latest.current = current;\n  const set = useCallback(\n    (next: Next | ((was: T) => Next)) => {\n      const resolved = typeof next === \"function\" ? (next as (was: T) => Next)(latest.current) : next;\n      latest.current = resolved;\n      if (value === undefined) setOwn(resolved);\n      onChange?.(resolved);\n    },\n    [value, onChange],\n  );\n  return [current, set] as const;\n}\n"},{"path":"beamline/hooks/use-deprecated-prop.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-deprecated-prop.ts","content":"\"use client\";\n\nimport { useEffect } from \"react\";\n\n// Bundlers replace NODE_ENV for browser builds; this declaration emits no Node dependency.\ndeclare const process: { env: { NODE_ENV?: string } };\n\nconst warned = new Set<string>();\n\n/** Report a legacy prop once after mount in development; production and server renders stay silent. */\nexport function useDeprecatedProp(component: string, prop: string, replacement: string, supplied: boolean) {\n  useEffect(() => {\n    if (process.env.NODE_ENV !== \"development\" || !supplied) return;\n    const key = `${component}.${prop}`;\n    if (warned.has(key)) return;\n    warned.add(key);\n    console.warn(`[Beamline] ${key} is deprecated. Use ${replacement} instead; the legacy prop still works.`);\n  }, [component, prop, replacement, supplied]);\n}\n"},{"path":"beamline/hooks/use-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-media-query.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-media-query.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// one MediaQueryList per query: React reads the snapshot on every render, and a part that renders thirty times a\n// second (a playing audio player's clock) would otherwise ask the browser for a new list each time\nconst lists = new Map<string, MediaQueryList>();\nconst list = (query: string) => {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return undefined;\n  let m = lists.get(query);\n  if (!m) lists.set(query, (m = window.matchMedia(query)));\n  return m;\n};\n\n/** True while the media query matches; false on the server and in the first client render of SSR output. */\nexport function useMediaQuery(query: string): boolean {\n  return useSyncExternalStore(\n    (notify) => {\n      const m = list(query);\n      m?.addEventListener(\"change\", notify);\n      return () => m?.removeEventListener(\"change\", notify);\n    },\n    () => list(query)?.matches ?? false,\n    () => false,\n  );\n}\n\n/** Phone-width layouts: below 768px. */\nexport const usePhone = () => useMediaQuery(\"(max-width: 767px)\");\n"},{"path":"beamline/hooks/use-motion-tokens.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-motion-tokens.ts","content":"\"use client\";\n\nimport { useMemo, type RefObject } from \"react\";\nimport { useAppearanceRoot } from \"../runtime/appearance\";\nimport { useResolvedAppearance } from \"./use-resolved-appearance\";\nimport { motionAt, motionEase, motionTokens, type MotionTokens } from \"../lib/motion-tokens\";\nimport { semanticMotion } from \"../lib/motion-source\";\n\nconst durationToken = (name: string) =>\n  name === \"exit\" ? \"motion-out\" : name === \"in\" || name === \"out\" ? `motion-${name}` : `duration-${name}`;\nconst easeToken = (name: string) => `ease-${name === \"inOut\" ? \"in-out\" : name}`;\nconst REQUEST = {\n  times: [\n    ...Object.keys(semanticMotion.duration).map(durationToken),\n    ...Object.keys(semanticMotion.spring).map((name) => `spring-${name}`),\n    ...Object.keys(semanticMotion.stagger).map((name) => `stagger-${name}`),\n    \"fade-in\",\n    \"fade-out\",\n  ],\n  easings: [...Object.keys(semanticMotion.ease).map(easeToken), \"ease-spring\", \"ease-spring-bounce\", \"ease-overshoot\"],\n  numbers: [\"motion-scale\", \"motion-look\", \"press-scale\"],\n  // timing only: the host's own type would make every control resolve differently from the page around it\n  typography: false,\n};\n\n/**\n * Resolve each timing at the actual host; omitted host uses the nearest managed root. The first/server snapshot is\n * the pure foundation default. Local CSS values, scope changes, font readiness, OS preference and explicit refresh\n * update the same consumer; no document-wide first-root fallback or persistent global listener.\n */\nexport function useMotionTokens(hostRef?: RefObject<HTMLElement | null>): MotionTokens & {\n  ready: boolean;\n  /** Reduced or none: movement, springs and staggers are instant. */\n  reduced: boolean;\n  mode: \"full\" | \"reduced\" | \"none\";\n  /** Opacity-only arrival and departure (seconds): kept under reduced, instant under none (APPEARANCE A06). */\n  fade: { in: number; out: number };\n} {\n  const root = useAppearanceRoot();\n  const scopeRef = useMemo(() => ({ current: root }), [root]);\n  const resolved = useResolvedAppearance(hostRef ?? scopeRef, REQUEST);\n  return useMemo(() => {\n    if (!resolved.ready)\n      return {\n        ...motionTokens,\n        ready: false,\n        reduced: false,\n        mode: \"full\" as const,\n        fade: { in: motionTokens.duration.in, out: motionTokens.duration.out },\n      };\n    const reduced = resolved.reducedMotion;\n    const speed = (resolved.numbers[\"motion-scale\"] ?? 1) * (resolved.numbers[\"motion-look\"] ?? 1);\n    const fade =\n      resolved.motionMode === \"none\"\n        ? { in: 0.001, out: 0.001 }\n        : {\n            in: resolved.times[\"fade-in\"] ?? semanticMotion.duration.in * speed,\n            out: resolved.times[\"fade-out\"] ?? semanticMotion.duration.out * speed,\n          };\n    const factor = reduced ? 0.001 / semanticMotion.duration.standard : speed;\n    const base = motionAt(factor);\n    const duration = Object.fromEntries(\n      Object.entries(base.duration).map(([name, fallback]) => [\n        name,\n        reduced ? 0.001 : (resolved.times[durationToken(name)] ?? fallback),\n      ]),\n    ) as MotionTokens[\"duration\"];\n    const stagger = Object.fromEntries(\n      Object.entries(base.stagger).map(([name, fallback]) => [\n        name,\n        reduced ? 0 : (resolved.times[`stagger-${name}`] ?? fallback),\n      ]),\n    ) as MotionTokens[\"stagger\"];\n    const ease = Object.fromEntries(\n      Object.entries(base.ease).map(([name, fallback]) => [\n        name,\n        motionEase(resolved.easings[easeToken(name)], fallback),\n      ]),\n    ) as MotionTokens[\"ease\"];\n    const spring = { ...base.spring };\n    for (const name of Object.keys(semanticMotion.spring) as (keyof typeof semanticMotion.spring)[])\n      spring[name] = {\n        ...base.spring[name],\n        visualDuration: reduced ? 0.001 : (resolved.times[`spring-${name}`] ?? base.spring[name].visualDuration),\n        bounce: reduced ? 0 : base.spring[name].bounce,\n      };\n    return {\n      ...base,\n      ready: true,\n      reduced,\n      mode: resolved.motionMode,\n      fade,\n      duration,\n      stagger,\n      ease,\n      spring,\n      pressScale: resolved.numbers[\"press-scale\"] ?? base.pressScale,\n      css: {\n        spring: resolved.easings[\"ease-spring\"] ?? base.css.spring,\n        springBounce: resolved.easings[\"ease-spring-bounce\"] ?? base.css.springBounce,\n        overshoot: resolved.easings[\"ease-overshoot\"] ?? base.css.overshoot,\n      },\n    };\n  }, [resolved]);\n}\n"},{"path":"beamline/hooks/use-reduced-motion-safe.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-reduced-motion-safe.ts","content":"\"use client\";\n\nimport { useMediaQuery } from \"./use-media-query\";\nimport { useScopeMotion } from \"../runtime/appearance\";\n\n/** Managed motion policy plus the OS preference, with a stable server snapshot. */\nexport function useReducedMotionSafe() {\n  const requested = useScopeMotion();\n  const os = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n  return requested !== \"auto\" || os;\n}\n"},{"path":"beamline/lib/auth.ts","type":"registry:file","target":"src/ui/beamline/lib/auth.ts","content":"import { createTranslator } from \"../runtime/messages\";\nimport { english } from \"../components/_shared/auth.messages\";\nimport type { ReactNode } from \"react\";\n\n/**\n * Shapes shared by the auth blocks (sign-in, signup-form and the auth page): the account a handler resolves with,\n * the single sign-on providers a product offers, and an error that points at one field. Your handlers talk to your\n * auth service; the blocks only show progress, results and messages.\n */\n\n/** Who signed in or signed up. */\nexport interface AuthAccount {\n  id?: string;\n  name: string;\n  email: string;\n  /** A photo URL; initials show without it. */\n  avatar?: string;\n}\n\n/** One single sign-on choice. Bring your own mark: the blocks ship no third-party logos. */\nexport interface AuthProvider {\n  id: string;\n  /** The provider's name as people know it: \"Google\", \"Microsoft\", \"SAML SSO\". */\n  label: string;\n  /** A generic icon, or a mark you are licensed to use; without one the button is text only. */\n  icon?: ReactNode;\n}\n\n/** The fields an auth handler can blame. */\nexport type AuthField = \"name\" | \"email\" | \"password\" | \"code\" | \"terms\";\n\n/**\n * Reject a handler's promise with this to show the message under one field instead of above the button:\n * `throw new AuthFieldError(\"email\", \"No account uses this email. Sign up instead?\")`.\n */\nexport class AuthFieldError extends Error {\n  readonly field: AuthField;\n  constructor(field: AuthField, message: string) {\n    super(message);\n    this.name = \"AuthFieldError\";\n    this.field = field;\n  }\n}\n\n/** The field an error points at, if it is an AuthFieldError (or any error carrying a `field`). */\nexport function authErrorField(error: unknown): AuthField | undefined {\n  const f = (error as { field?: unknown } | null)?.field;\n  return f === \"name\" || f === \"email\" || f === \"password\" || f === \"code\" || f === \"terms\" ? f : undefined;\n}\n\n/** A message for any rejection: the error's own words, or a plain fallback. */\nexport function authErrorMessage(error: unknown, fallback = createTranslator(\"_shared-auth\", english)(\"failure\")) {\n  return error instanceof Error && error.message\n    ? error.message\n    : typeof error === \"string\" && error\n      ? error\n      : fallback;\n}\n\n/** A complete-looking address: something@domain.tld, no spaces. The server has the final word. */\nexport const isEmailAddress = (value: string) => /^[^\\s@]+@[^\\s@.]+(\\.[^\\s@.]+)+$/.test(value.trim());\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":"Sign up form is in src/ui/beamline/blocks/signup-form/signup-form.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/signup-form"}}