{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sign-in-flow",
  "type": "registry:ui",
  "title": "Sign-in Flow",
  "description": "Email first, then a password or emailed code, in a card that glides between heights.",
  "categories": [
    "onboarding"
  ],
  "dependencies": [
    "@base-ui/react",
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/sign-in-flow.tsx",
      "content": "\"use client\";\nimport { Field } from \"@base-ui/react/field\";\nimport { OTPField } from \"@base-ui/react/otp-field\";\nimport NumberFlow from \"@number-flow/react\";\nimport { AnimatePresence, animate, motion, useIsPresent, useReducedMotion, type AnimationPlaybackControls } from \"motion/react\";\nimport { Fragment, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Alert, ArrowLeft, Eye, EyeOff } from \"@/lib/icons\";\nimport { ease, spring, swap } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\nexport type SignInMethod = \"password\" | \"code\";\nexport type SignInStep = \"email\" | \"password\" | \"code\" | \"done\";\ntype Busy = null | \"email\" | \"password\" | \"code\" | \"resend\" | \"switch\";\ntype FieldName = \"email\" | \"password\" | \"code\";\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst rank: Record<SignInStep, number> = { email: 0, password: 1, code: 1, done: 2 };\n\nconst messageOf = (error: unknown, fallback: string) =>\n  error instanceof Error && error.message ? error.message : typeof error === \"string\" && error ? error : fallback;\n\n/** Tracks Caps Lock from key events on a field. Browsers only report it while a key is pressed. */\nexport function useCapsLock() {\n  const [capsLock, setCapsLock] = useState(false);\n  const onKey = useCallback((e: React.KeyboardEvent) => {\n    // getModifierState is missing on some synthetic events from autofill.\n    if (typeof e.getModifierState === \"function\") setCapsLock(e.getModifierState(\"CapsLock\"));\n  }, []);\n  const reset = useCallback(() => setCapsLock(false), []);\n  return { capsLock, onKey, reset };\n}\n\nexport type SignInFlowProps = Omit<React.ComponentProps<\"div\">, \"title\" | \"onSubmit\" | \"children\"> & {\n  /** Heading on the email step. */\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  step?: SignInStep;\n  defaultStep?: SignInStep;\n  onStepChange?: (step: SignInStep) => void;\n  defaultEmail?: string;\n  /**\n   * Look the account up. Resolve the method it signs in with; throw (or reject) with a\n   * message to show it under the email field. Resolving nothing uses `defaultMethod`.\n   */\n  onEmailSubmit?: (email: string) => Promise<SignInMethod | void> | SignInMethod | void;\n  /** Throw with a message to show it under the password field. */\n  onPasswordSubmit?: (email: string, password: string) => Promise<void> | void;\n  /** Throw with a message to show it under the code. */\n  onCodeSubmit?: (email: string, code: string) => Promise<void> | void;\n  /** Send a new code. Enables resending, and \"Email me a code\" on the password step. */\n  onSendCode?: (email: string) => Promise<void> | void;\n  onForgotPassword?: (email: string) => void;\n  /** Called once a password or code is accepted, after the done state appears. */\n  onSuccess?: (email: string) => void;\n  defaultMethod?: SignInMethod;\n  codeLength?: number;\n  /** Seconds before another code can be requested. */\n  resendCooldown?: number;\n  /** Sign-in buttons for other providers, shown above the email field with an \"or\" rule. */\n  providers?: React.ReactNode;\n  /** Shown under the email step, e.g. a link to create an account. */\n  footer?: React.ReactNode;\n  doneTitle?: React.ReactNode;\n  doneDescription?: React.ReactNode;\n};\n\nexport function SignInFlow({\n  title = \"Sign in\",\n  description,\n  step: stepProp,\n  defaultStep = \"email\",\n  onStepChange,\n  defaultEmail = \"\",\n  onEmailSubmit,\n  onPasswordSubmit,\n  onCodeSubmit,\n  onSendCode,\n  onForgotPassword,\n  onSuccess,\n  defaultMethod = \"password\",\n  codeLength = 6,\n  resendCooldown = 30,\n  providers,\n  footer,\n  doneTitle = \"Signed in\",\n  doneDescription = \"Taking you to your workspace…\",\n  className,\n  ...rest\n}: SignInFlowProps) {\n  const reduce = useReducedMotion();\n  const uid = useId();\n  const [step, setStep] = useControllableState<SignInStep>({ value: stepProp, defaultValue: defaultStep, onChange: onStepChange });\n  const [email, setEmail] = useState(defaultEmail);\n  const [password, setPassword] = useState(\"\");\n  const [code, setCode] = useState(\"\");\n  const [busy, setBusy] = useState<Busy>(null);\n  const [error, setError] = useState<{ field: FieldName; message: string } | null>(null);\n  const [codeRejected, setCodeRejected] = useState(false);\n  const [announcement, setAnnouncement] = useState(\"\");\n  const [cooldown, setCooldown] = useState({ until: 0, now: 0 });\n  const [sent, setSent] = useState(false);\n\n  // Direction of travel: forward slides in from the right, back from the left.\n  const [shownStep, setShownStep] = useState(step);\n  const [dir, setDir] = useState(1);\n  if (shownStep !== step) {\n    setShownStep(step);\n    setDir(rank[step] >= rank[shownStep] ? 1 : -1);\n  }\n\n  // Each request gets a ticket. Going back or starting another one voids the old ticket,\n  // so a slow answer for an address you've already changed can't move you forward.\n  const ticket = useRef(0);\n  const timers = useRef<number[]>([]);\n  useEffect(() => () => timers.current.forEach((t) => window.clearTimeout(t)), []);\n  const later = (fn: () => void, ms: number) => timers.current.push(window.setTimeout(fn, ms));\n\n  const card = useRef<HTMLDivElement>(null);\n  const emailRef = useRef<HTMLElement>(null);\n  const passwordRef = useRef<HTMLElement>(null);\n  const codeRef = useRef<HTMLElement>(null);\n  const doneRef = useRef<HTMLElement>(null);\n  // Read only in effects and handlers.\n  const stepEl = (s: SignInStep) => ({ email: emailRef, password: passwordRef, code: codeRef, done: doneRef })[s].current;\n  const settled = useRef(0);\n  const moving = useRef<AnimationPlaybackControls | null>(null);\n  const lastStep = useRef(step);\n\n  // At rest the card is height:auto, so errors and hints can grow it freely.\n  useEffect(() => {\n    const el = card.current;\n    if (!el) return;\n    const ro = new ResizeObserver(() => {\n      if (!moving.current) settled.current = el.offsetHeight;\n    });\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  // Between steps the card glides from the old height to the new one. An interrupted\n  // glide starts again from wherever it is now.\n  useLayoutEffect(() => {\n    if (lastStep.current === step) return;\n    lastStep.current = step;\n    const el = card.current;\n    const target = stepEl(step);\n    if (!el || !target || reduce) return;\n    const cs = getComputedStyle(el);\n    const chrome = [\"paddingTop\", \"paddingBottom\", \"borderTopWidth\", \"borderBottomWidth\"].reduce((n, k) => n + parseFloat(cs[k as \"paddingTop\"]), 0);\n    const from = moving.current ? el.offsetHeight : settled.current;\n    const to = chrome + target.offsetHeight;\n    moving.current?.stop();\n    if (!from || Math.abs(from - to) < 1) return;\n    const controls = animate(el, { height: [from, to] }, { duration: 0.34, ease: ease.inOut });\n    moving.current = controls;\n    controls.then(() => {\n      if (moving.current !== controls) return;\n      moving.current = null;\n      el.style.height = \"\";\n      settled.current = el.offsetHeight;\n    });\n  }, [step, reduce]);\n\n  // Focus follows the step, so the keyboard never has to hunt for the next field.\n  const focusedStep = useRef(step);\n  useEffect(() => {\n    if (focusedStep.current === step) return;\n    focusedStep.current = step;\n    const el = stepEl(step)?.querySelector<HTMLElement>(\"[data-autofocus]\");\n    el?.focus({ preventScroll: true });\n    if (el instanceof HTMLInputElement && step === \"email\") el.select();\n  }, [step]);\n\n  const go = (next: SignInStep) => {\n    setError(null);\n    setCodeRejected(false);\n    setStep(next);\n  };\n  const fail = (field: FieldName, message: string) => {\n    setError({ field, message });\n    setBusy(null);\n  };\n  const startCooldown = () => {\n    const now = Date.now();\n    setCooldown({ until: now + resendCooldown * 1000, now });\n  };\n\n  // The countdown is measured against the clock, not counted, so a throttled\n  // background tab still lands on the right second.\n  useEffect(() => {\n    if (!cooldown.until) return;\n    const id = window.setInterval(() => {\n      const now = Date.now();\n      setCooldown((c) => ({ ...c, now }));\n      if (now >= cooldown.until) window.clearInterval(id);\n    }, 250);\n    return () => window.clearInterval(id);\n  }, [cooldown.until]);\n  const remaining = Math.max(0, Math.ceil((cooldown.until - cooldown.now) / 1000));\n\n  const succeed = () => {\n    setBusy(null);\n    go(\"done\");\n    setAnnouncement(`Signed in as ${email.trim()}`);\n    onSuccess?.(email.trim());\n  };\n\n  async function submitEmail(e: React.FormEvent) {\n    e.preventDefault();\n    if (busy) return;\n    const value = email.trim();\n    if (!value) return fail(\"email\", \"Enter your email address\");\n    if (!EMAIL.test(value)) return fail(\"email\", \"Enter a full email address, like name@company.com\");\n    const mine = ++ticket.current;\n    setBusy(\"email\");\n    setError(null);\n    try {\n      const method = (await onEmailSubmit?.(value)) || defaultMethod;\n      if (mine !== ticket.current) return;\n      setEmail(value);\n      setPassword(\"\");\n      setCode(\"\");\n      setBusy(null);\n      if (method === \"code\") startCooldown();\n      go(method);\n      setAnnouncement(method === \"code\" ? `We sent a ${codeLength}-digit code to ${value}` : `Enter the password for ${value}`);\n    } catch (err) {\n      if (mine === ticket.current) fail(\"email\", messageOf(err, \"Couldn’t check that address. Try again.\"));\n    }\n  }\n\n  async function submitPassword(e: React.FormEvent) {\n    e.preventDefault();\n    if (busy) return;\n    if (!password) return fail(\"password\", \"Enter your password\");\n    const mine = ++ticket.current;\n    setBusy(\"password\");\n    setError(null);\n    try {\n      await onPasswordSubmit?.(email.trim(), password);\n      if (mine === ticket.current) succeed();\n    } catch (err) {\n      if (mine !== ticket.current) return;\n      fail(\"password\", messageOf(err, \"That password isn’t right. Try again or reset it.\"));\n      // Keep what they typed but select it, so the next attempt replaces it in one go.\n      const input = stepEl(\"password\")?.querySelector<HTMLInputElement>(\"[data-autofocus]\");\n      input?.focus();\n      input?.select();\n    }\n  }\n\n  async function submitCode(value: string) {\n    if (busy === \"code\") return;\n    if (value.length < codeLength) return fail(\"code\", `Enter all ${codeLength} digits`);\n    const mine = ++ticket.current;\n    setBusy(\"code\");\n    setError(null);\n    try {\n      await onCodeSubmit?.(email.trim(), value);\n      if (mine === ticket.current) succeed();\n    } catch (err) {\n      if (mine !== ticket.current) return;\n      fail(\"code\", messageOf(err, \"That code didn’t work. Check it, or send a new one.\"));\n      setCodeRejected(true);\n      // Hold the wrong code long enough to be seen, then clear it for the next try.\n      later(() => {\n        if (mine !== ticket.current) return;\n        setCode(\"\");\n        setCodeRejected(false);\n        stepEl(\"code\")?.querySelector<HTMLElement>(\"[data-autofocus]\")?.focus();\n      }, 900);\n    }\n  }\n\n  async function sendCode(from: \"resend\" | \"switch\") {\n    if (busy || !onSendCode) return;\n    const mine = ++ticket.current;\n    setBusy(from);\n    setError(null);\n    try {\n      await onSendCode(email.trim());\n      if (mine !== ticket.current) return;\n      setBusy(null);\n      setCode(\"\");\n      startCooldown();\n      setAnnouncement(`We sent a new code to ${email.trim()}`);\n      if (from === \"switch\") go(\"code\");\n      else {\n        setSent(true);\n        later(() => setSent(false), 1600);\n      }\n    } catch (err) {\n      if (mine === ticket.current) fail(from === \"switch\" ? \"password\" : \"code\", messageOf(err, \"Couldn’t send a code. Try again in a moment.\"));\n    }\n  }\n\n  const back = () => {\n    ticket.current++;\n    setBusy(null);\n    setPassword(\"\");\n    setCode(\"\");\n    go(\"email\");\n    setAnnouncement(\"\");\n  };\n\n  const errorFor = (f: FieldName) => (error?.field === f ? error.message : undefined);\n  const identity = <Identity email={email.trim()} />;\n  const backRow = (right?: React.ReactNode) => (\n    <div className=\"flex min-h-7 items-center justify-between gap-3\">\n      <TextButton onClick={back} className=\"group/back\">\n        <ArrowLeft size={14} className=\"shrink-0 transition-transform duration-200 ease-out group-hover/back:-translate-x-0.5\" />\n        Use a different email\n      </TextButton>\n      {right}\n    </div>\n  );\n\n  return (\n    <div\n      ref={card}\n      data-step={step}\n      className={cn(\n        \"relative w-full max-w-[360px] overflow-hidden rounded-xl border border-line bg-raised p-5 text-left shadow-[var(--shadow)] sm:p-6\",\n        className,\n      )}\n      {...rest}\n    >\n      <div className=\"relative\">\n        <AnimatePresence initial={false} mode=\"popLayout\" custom={dir}>\n          {step === \"email\" && (\n            <Step key=\"email\" direction={dir} reduce={!!reduce} ref={emailRef}>\n              <Header title={title} description={description} />\n              {providers && (\n                <>\n                  <div className=\"flex flex-col gap-2\">{providers}</div>\n                  <div className=\"flex items-center gap-3\" aria-hidden>\n                    <span className=\"h-px flex-1 bg-line\" />\n                    <span className=\"font-mono text-2xs uppercase tracking-[0.08em] text-fg-4\">or</span>\n                    <span className=\"h-px flex-1 bg-line\" />\n                  </div>\n                </>\n              )}\n              <form noValidate onSubmit={submitEmail} aria-busy={busy === \"email\" || undefined} className=\"flex flex-col gap-4\">\n                <Field.Root invalid={!!errorFor(\"email\")} className=\"flex flex-col gap-1.5\">\n                  <Field.Label className=\"w-fit text-[12.5px] font-medium text-fg\">Email</Field.Label>\n                  <Field.Control\n                    data-autofocus\n                    type=\"email\"\n                    name=\"email\"\n                    autoComplete=\"username\"\n                    inputMode=\"email\"\n                    autoCapitalize=\"none\"\n                    spellCheck={false}\n                    enterKeyHint=\"next\"\n                    placeholder=\"name@company.com\"\n                    value={email}\n                    readOnly={busy === \"email\"}\n                    onValueChange={(v) => {\n                      setEmail(v);\n                      if (error?.field === \"email\") setError(null);\n                    }}\n                    className={inputClass}\n                  />\n                  <Message message={errorFor(\"email\")} reduce={!!reduce} />\n                </Field.Root>\n                <Submit busy={busy === \"email\"}>Continue</Submit>\n              </form>\n              {footer && <div className=\"text-center text-[12.5px] text-fg-3\">{footer}</div>}\n            </Step>\n          )}\n\n          {step === \"password\" && (\n            <Step key=\"password\" direction={dir} reduce={!!reduce} ref={passwordRef}>\n              <Header title=\"Enter your password\">{identity}</Header>\n              <PasswordForm\n                email={email.trim()}\n                password={password}\n                setPassword={(v) => {\n                  setPassword(v);\n                  if (error?.field === \"password\") setError(null);\n                }}\n                busy={busy === \"password\"}\n                error={errorFor(\"password\")}\n                reduce={!!reduce}\n                onSubmit={submitPassword}\n                onForgot={onForgotPassword ? () => onForgotPassword(email.trim()) : undefined}\n              />\n              {backRow(\n                onSendCode && onCodeSubmit ? (\n                  <TextButton onClick={() => sendCode(\"switch\")} aria-busy={busy === \"switch\" || undefined} className=\"relative\">\n                    <span className={cn(\"transition-opacity duration-150\", busy === \"switch\" && \"opacity-0\")}>Email me a code</span>\n                    {busy === \"switch\" && (\n                      <span className=\"absolute inset-0 grid place-items-center\">\n                        <Spinner />\n                      </span>\n                    )}\n                  </TextButton>\n                ) : null,\n              )}\n            </Step>\n          )}\n\n          {step === \"code\" && (\n            <Step key=\"code\" direction={dir} reduce={!!reduce} ref={codeRef}>\n              <Header title=\"Check your email\" description={`Enter the ${codeLength}-digit code we sent to`}>\n                {identity}\n              </Header>\n              <form\n                noValidate\n                onSubmit={(e) => {\n                  e.preventDefault();\n                  submitCode(code);\n                }}\n                aria-busy={busy === \"code\" || undefined}\n                className=\"flex flex-col gap-4\"\n              >\n                <Field.Root invalid={!!errorFor(\"code\")} className=\"flex flex-col gap-1.5\">\n                  <Field.Label className=\"sr-only\">Sign-in code</Field.Label>\n                  <CodeCells\n                    id={`${uid}-code`}\n                    length={codeLength}\n                    value={code}\n                    onChange={(v) => {\n                      setCode(v);\n                      if (error?.field === \"code\" && !codeRejected) setError(null);\n                    }}\n                    onComplete={submitCode}\n                    busy={busy === \"code\"}\n                    rejected={codeRejected}\n                    reduce={!!reduce}\n                  />\n                  <Message message={errorFor(\"code\")} reduce={!!reduce} />\n                </Field.Root>\n                <Submit busy={busy === \"code\"}>Verify code</Submit>\n              </form>\n              {backRow(\n                onSendCode ? (\n                  <Resend remaining={remaining} busy={busy === \"resend\"} sent={sent} disabled={!!busy && busy !== \"resend\"} onResend={() => sendCode(\"resend\")} />\n                ) : null,\n              )}\n            </Step>\n          )}\n\n          {step === \"done\" && (\n            <Step key=\"done\" direction={dir} reduce={!!reduce} ref={doneRef} className=\"items-center gap-4 py-3 text-center\">\n              <DoneMark reduce={!!reduce} />\n              <div className=\"flex max-w-full flex-col items-center gap-1\">\n                <h2 data-autofocus tabIndex={-1} className=\"text-[15px] font-medium tracking-[-0.015em] text-fg outline-none\">\n                  {doneTitle}\n                </h2>\n                <p className=\"max-w-full truncate text-[12.5px] text-fg-2\" title={email.trim()}>\n                  {email.trim()}\n                </p>\n                {doneDescription && <p className=\"mt-2 text-[12px] text-fg-3\">{doneDescription}</p>}\n              </div>\n            </Step>\n          )}\n        </AnimatePresence>\n      </div>\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n\nconst inputClass = cn(\n  \"h-9 w-full min-w-0 rounded-lg border border-line-2 bg-raised px-3 text-base text-fg outline-none sm:text-[13px]\",\n  \"placeholder:text-fg-4 transition-[border-color,box-shadow] duration-150 ease-out\",\n  \"hover:border-fg-4 focus:border-fg-3 focus:ring-3 focus:ring-fg/8\",\n  \"read-only:text-fg-2 data-invalid:border-danger/60 data-invalid:focus:border-danger/80 data-invalid:focus:ring-danger/15\",\n);\n\ntype StepProps = { direction: number; reduce: boolean; className?: string; children: React.ReactNode; ref?: React.Ref<HTMLElement> };\n\n// One step of the flow. Leaving steps go inert at once so a stray click or Tab\n// can't land on something already on its way out.\nfunction Step({ direction, reduce, className, children, ref }: StepProps) {\n  const present = useIsPresent();\n  const variants = {\n    enter: (d: number) => (reduce ? { opacity: 0 } : { opacity: 0, x: 20 * d, filter: \"blur(2px)\" }),\n    center: { opacity: 1, x: 0, filter: \"blur(0px)\" },\n    // The old step clears out quickly so the two never read as one muddy layer.\n    exit: (d: number) =>\n      reduce\n        ? { opacity: 0, transition: { duration: 0.1 } }\n        : { opacity: 0, x: -12 * d, filter: \"blur(2px)\", transition: { duration: 0.14, ease: ease.in } },\n  };\n  return (\n    <motion.section\n      ref={ref}\n      custom={direction}\n      variants={variants}\n      initial=\"enter\"\n      animate=\"center\"\n      exit=\"exit\"\n      inert={!present}\n      transition={\n        reduce\n          ? { duration: 0.15 }\n          : { duration: 0.32, ease: ease.out, delay: 0.07, filter: { duration: 0.2, delay: 0.07 } }\n      }\n      className={cn(\"flex w-full flex-col gap-5\", className)}\n    >\n      {children}\n    </motion.section>\n  );\n}\n\nfunction Header({ title, description, children }: { title: React.ReactNode; description?: React.ReactNode; children?: React.ReactNode }) {\n  return (\n    <div className=\"flex flex-col gap-1\">\n      <h2 className=\"text-balance text-[15px] font-medium leading-5 tracking-[-0.015em] text-fg\">{title}</h2>\n      {description && <p className=\"text-pretty text-[12.5px] leading-[18px] text-fg-2\">{description}</p>}\n      {children && <div className=\"mt-1.5 flex min-w-0\">{children}</div>}\n    </div>\n  );\n}\n\n// The account you're signing in to stays visible on every step after the first.\nfunction Identity({ email }: { email: string }) {\n  return (\n    <span className=\"inline-flex h-7 min-w-0 max-w-full items-center gap-2 rounded-full border border-line bg-frame pl-1 pr-3\" title={email}>\n      <span aria-hidden className=\"grid size-5 shrink-0 place-items-center rounded-full bg-line-2 text-[10.5px] font-medium uppercase text-fg\">\n        {email.charAt(0) || \"?\"}\n      </span>\n      <span className=\"min-w-0 truncate text-[12.5px] text-fg\">{email}</span>\n    </span>\n  );\n}\n\nfunction PasswordForm({\n  email,\n  password,\n  setPassword,\n  busy,\n  error,\n  reduce,\n  onSubmit,\n  onForgot,\n}: {\n  email: string;\n  password: string;\n  setPassword: (v: string) => void;\n  busy: boolean;\n  error?: string;\n  reduce: boolean;\n  onSubmit: (e: React.FormEvent) => void;\n  onForgot?: () => void;\n}) {\n  const [shown, setShown] = useState(false);\n  const { capsLock, onKey, reset } = useCapsLock();\n  const EyeIcon = shown ? EyeOff : Eye;\n  return (\n    <form noValidate onSubmit={onSubmit} aria-busy={busy || undefined} className=\"flex flex-col gap-4\">\n      {/* Lets password managers pair the saved password with the right account. */}\n      <input type=\"email\" name=\"username\" autoComplete=\"username\" value={email} readOnly tabIndex={-1} aria-hidden className=\"sr-only\" />\n      <Field.Root invalid={!!error} className=\"flex flex-col gap-1.5\">\n        <div className=\"flex items-center justify-between gap-3\">\n          <Field.Label className=\"w-fit text-[12.5px] font-medium text-fg\">Password</Field.Label>\n          {onForgot && (\n            <button\n              type=\"button\"\n              onClick={onForgot}\n              className=\"relative -my-1 rounded-sm text-[12px] text-fg-3 outline-none transition-colors duration-150 before:absolute before:-inset-x-1 before:-inset-y-2 before:content-[''] hover:text-fg focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\"\n            >\n              Forgot password?\n            </button>\n          )}\n        </div>\n        <div className=\"relative\">\n          <Field.Control\n            data-autofocus\n            type={shown ? \"text\" : \"password\"}\n            name=\"password\"\n            autoComplete=\"current-password\"\n            autoCapitalize=\"none\"\n            spellCheck={false}\n            enterKeyHint=\"go\"\n            value={password}\n            readOnly={busy}\n            onValueChange={(v) => setPassword(v)}\n            onKeyDown={onKey}\n            onKeyUp={onKey}\n            onBlur={reset}\n            className={cn(inputClass, \"pr-10\")}\n          />\n          <button\n            type=\"button\"\n            aria-label=\"Show password\"\n            aria-pressed={shown}\n            onClick={() => setShown((s) => !s)}\n            className={cn(\n              \"absolute right-1 top-1/2 grid size-7 -translate-y-1/2 place-items-center rounded-md text-fg-3 outline-none\",\n              \"transition-[background-color,color,scale] duration-150 ease-out hover:bg-hover hover:text-fg active:scale-[0.92]\",\n              \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-0 focus-visible:outline-fg-3\",\n              \"before:absolute before:-inset-2 before:content-[''] pointer-fine:before:hidden\",\n            )}\n          >\n            <span className=\"relative grid size-4 place-items-center\">\n              <AnimatePresence initial={false}>\n                <motion.span\n                  key={shown ? \"hide\" : \"show\"}\n                  className=\"absolute inset-0 grid place-items-center\"\n                  initial={reduce ? { opacity: 0 } : swap.initial}\n                  animate={swap.animate}\n                  exit={reduce ? { opacity: 0 } : swap.exit}\n                  transition={reduce ? { duration: 0.12 } : spring.pop}\n                >\n                  <EyeIcon />\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          </button>\n        </div>\n        <Message message={error} reduce={reduce} />\n        <Collapse open={capsLock && !error} reduce={reduce}>\n          <p className=\"flex items-center gap-1.5 pt-0.5 text-[12px] text-fg-2\">\n            <CapsGlyph />\n            Caps Lock is on\n          </p>\n        </Collapse>\n      </Field.Root>\n      <Submit busy={busy}>Sign in</Submit>\n    </form>\n  );\n}\n\n// Height-to-content reveal, so a message pushes the button down smoothly instead of snapping it.\nfunction Collapse({ open, reduce, children }: { open: boolean; reduce: boolean; children: React.ReactNode }) {\n  return (\n    <AnimatePresence initial={false}>\n      {open && (\n        <motion.div\n          initial={{ height: 0, opacity: 0 }}\n          animate={{ height: \"auto\", opacity: 1 }}\n          exit={{ height: 0, opacity: 0, transition: { duration: reduce ? 0.1 : 0.16, ease: ease.in } }}\n          transition={reduce ? { duration: 0.12, height: { duration: 0 } } : { duration: 0.22, ease: ease.out }}\n          className=\"overflow-hidden\"\n        >\n          <motion.div initial={reduce ? false : { y: -4 }} animate={{ y: 0 }} transition={{ duration: 0.22, ease: ease.out }}>\n            {children}\n          </motion.div>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction Message({ message, reduce }: { message?: string; reduce: boolean }) {\n  return (\n    <Collapse open={!!message} reduce={reduce}>\n      <Field.Error match className=\"flex items-start gap-1.5 pt-0.5 text-[12px] leading-4 text-danger\">\n        <Alert size={14} className=\"mt-px shrink-0\" />\n        <span className=\"min-w-0 text-pretty\">{message}</span>\n      </Field.Error>\n    </Collapse>\n  );\n}\n\nfunction Submit({ busy, children }: { busy: boolean; children: React.ReactNode }) {\n  return (\n    <button\n      type=\"submit\"\n      aria-disabled={busy || undefined}\n      data-busy={busy ? \"\" : undefined}\n      onClick={(e) => busy && e.preventDefault()}\n      className={cn(\n        \"relative grid h-9 w-full select-none place-items-center rounded-lg bg-fg px-3 text-[13px] font-medium tracking-[-0.005em] text-frame outline-none\",\n        \"transition-[background-color,scale] duration-150 ease-out hover:bg-fg/90 active:scale-[0.985] active:duration-75\",\n        \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n        \"data-busy:cursor-default data-busy:bg-fg/85 data-busy:active:scale-100\",\n      )}\n    >\n      <span\n        className={cn(\n          \"col-start-1 row-start-1 transition-[opacity,scale,filter] duration-150 ease-out\",\n          busy && \"scale-95 opacity-0 blur-[2px] motion-reduce:scale-100 motion-reduce:blur-none\",\n        )}\n      >\n        {children}\n      </span>\n      <span aria-hidden className={cn(\"col-start-1 row-start-1 grid place-items-center transition-opacity duration-150\", busy ? \"opacity-100\" : \"opacity-0\")}>\n        {busy && <Spinner />}\n      </span>\n      {busy && <span className=\"sr-only\">Working…</span>}\n    </button>\n  );\n}\n\nfunction Spinner() {\n  return (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden className=\"animate-spin [animation-duration:0.7s] motion-reduce:[animation-duration:1.6s]!\">\n      <circle cx=\"8\" cy=\"8\" r=\"5.75\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"1.5\" />\n      <path d=\"M13.75 8A5.75 5.75 0 0 0 8 2.25\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n\nfunction TextButton({ className, children, ...rest }: React.ComponentProps<\"button\">) {\n  return (\n    <button\n      type=\"button\"\n      className={cn(\n        \"-mx-1.5 inline-flex h-7 min-w-0 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md px-1.5 text-[12.5px] text-fg-2 outline-none\",\n        \"transition-[background-color,color,scale] duration-150 ease-out hover:bg-hover hover:text-fg active:scale-[0.97]\",\n        \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n        \"disabled:pointer-events-none disabled:text-fg-3\",\n        className,\n      )}\n      {...rest}\n    >\n      {children}\n    </button>\n  );\n}\n\n// The resend control lives in one fixed-width cell, so the countdown, \"Sending…\" and \"Code sent\" swap without moving the row.\nfunction Resend({ remaining, busy, sent, disabled, onResend }: { remaining: number; busy: boolean; sent: boolean; disabled: boolean; onResend: () => void }) {\n  const reduce = useReducedMotion();\n  const state = busy ? \"busy\" : sent ? \"sent\" : remaining > 0 ? \"wait\" : \"ready\";\n  const labels = [\"Resend in 0:00\", \"Resend code\", \"Sending…\", \"Code sent\"];\n  return (\n    <TextButton\n      onClick={onResend}\n      disabled={disabled || state !== \"ready\"}\n      aria-busy={busy || undefined}\n      aria-label={state === \"wait\" ? `Resend code, available in ${remaining} seconds` : undefined}\n      className=\"justify-end disabled:text-fg-3\"\n    >\n      <span className=\"grid justify-items-end\">\n        {labels.map((l) => (\n          <span key={l} aria-hidden className=\"invisible col-start-1 row-start-1 tabular\">\n            {l}\n          </span>\n        ))}\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            key={state}\n            className=\"col-start-1 row-start-1 inline-flex items-center gap-1 tabular\"\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 5, filter: \"blur(2px)\" }}\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -5, filter: \"blur(2px)\", transition: { duration: 0.12 } }}\n            transition={{ duration: reduce ? 0.12 : 0.2, ease: ease.out }}\n          >\n            {state === \"wait\" && (\n              <span>\n                Resend in 0:\n                <NumberFlow value={remaining} format={{ minimumIntegerDigits: 2 }} trend={-1} animated={!reduce} />\n              </span>\n            )}\n            {state === \"ready\" && \"Resend code\"}\n            {state === \"busy\" && \"Sending…\"}\n            {state === \"sent\" && (\n              <>\n                <svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className=\"text-success\">\n                  <motion.path d=\"M3.5 8.5 6.5 11.5 12.5 4.5\" initial={reduce ? false : { pathLength: 0 }} animate={{ pathLength: 1 }} transition={{ duration: 0.3, ease: ease.out, delay: 0.05 }} />\n                </svg>\n                Code sent\n              </>\n            )}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </TextButton>\n  );\n}\n\nfunction CodeCells({\n  id,\n  length,\n  value,\n  onChange,\n  onComplete,\n  busy,\n  rejected,\n  reduce,\n}: {\n  id: string;\n  length: number;\n  value: string;\n  onChange: (v: string) => void;\n  onComplete: (v: string) => void;\n  busy: boolean;\n  rejected: boolean;\n  reduce: boolean;\n}) {\n  const [active, setActive] = useState<number | null>(null);\n  const group = length % 3 === 0 && length > 3 ? 3 : length % 4 === 0 && length > 4 ? 4 : 0;\n  return (\n    <OTPField.Root\n      id={id}\n      length={length}\n      value={value}\n      onValueChange={(v) => onChange(v)}\n      onValueComplete={(v) => onComplete(v)}\n      readOnly={busy || rejected}\n      className=\"flex w-full items-center gap-1.5 sm:gap-2\"\n    >\n      {Array.from({ length }, (_, i) => {\n        const char = value[i] ?? \"\";\n        const caret = active === i && !char && !busy;\n        return (\n          <Fragment key={i}>\n            {group > 0 && i > 0 && i % group === 0 && <OTPField.Separator className=\"h-px w-2 shrink-0 rounded-full bg-fg-4\" />}\n            <div className=\"relative h-11 min-w-0 flex-1\">\n              <OTPField.Input\n                data-autofocus={i === 0 ? \"\" : undefined}\n                aria-label={i === 0 ? undefined : `Digit ${i + 1} of ${length}`}\n                onFocus={() => setActive(i)}\n                onBlur={() => setActive((a) => (a === i ? null : a))}\n                className={cn(\n                  \"block size-full rounded-lg border bg-raised text-center text-transparent caret-transparent outline-none selection:bg-transparent! selection:text-transparent!\",\n                  \"transition-[border-color,background-color,box-shadow] duration-150 ease-out\",\n                  rejected\n                    ? \"border-danger/60 bg-danger-soft\"\n                    : cn(\"hover:border-fg-4 focus:border-fg-3 focus:ring-3 focus:ring-fg/8\", char ? \"border-fg-4\" : \"border-line-2\"),\n                  \"read-only:cursor-default\",\n                )}\n              />\n              <span aria-hidden className={cn(\"pointer-events-none absolute inset-0 grid place-items-center text-[17px] font-medium tabular\", rejected ? \"text-danger\" : \"text-fg\")}>\n                <AnimatePresence initial={false}>\n                  {char && (\n                    <motion.span\n                      key={char + i}\n                      className=\"col-start-1 row-start-1\"\n                      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6, scale: 0.85, filter: \"blur(2px)\" }}\n                      animate={\n                        busy && !reduce\n                          ? { opacity: [1, 0.35, 1], y: 0, scale: 1, filter: \"blur(0px)\" }\n                          : { opacity: 1, y: 0, scale: 1, filter: \"blur(0px)\" }\n                      }\n                      exit={reduce ? { opacity: 0, transition: { duration: 0.08 } } : { opacity: 0, scale: 0.7, filter: \"blur(2px)\", transition: { duration: 0.1, ease: ease.in } }}\n                      transition={\n                        busy && !reduce\n                          ? { opacity: { duration: 1, repeat: Infinity, ease: \"easeInOut\", delay: i * 0.08 } }\n                          : reduce\n                            ? { duration: 0.12 }\n                            : spring.pop\n                      }\n                    >\n                      {char}\n                    </motion.span>\n                  )}\n                </AnimatePresence>\n                {caret && <span className=\"col-start-1 row-start-1 h-5 w-px animate-caret bg-fg\" />}\n              </span>\n            </div>\n          </Fragment>\n        );\n      })}\n    </OTPField.Root>\n  );\n}\n\nfunction DoneMark({ reduce }: { reduce: boolean }) {\n  const draw = (delay: number) =>\n    reduce\n      ? { initial: false as const }\n      : { initial: { pathLength: 0, opacity: 0 }, animate: { pathLength: 1, opacity: 1 }, transition: { duration: 0.4, ease: ease.out, delay, opacity: { duration: 0.01, delay } } };\n  return (\n    <motion.span\n      aria-hidden\n      className=\"grid size-11 place-items-center rounded-full bg-success-soft text-success\"\n      initial={reduce ? false : { scale: 0.8, opacity: 0 }}\n      animate={{ scale: 1, opacity: 1 }}\n      transition={spring.bouncy}\n    >\n      <svg width=\"22\" height=\"22\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.4} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n        <motion.path d=\"M4 8.4 6.8 11.2 12 5\" {...draw(0.12)} />\n      </svg>\n    </motion.span>\n  );\n}\n\nfunction CapsGlyph() {\n  return (\n    <svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.4} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className=\"shrink-0 text-warning\">\n      <path d=\"M8 2.75 3.25 7.75h2.5v3h4.5v-3h2.5z\" />\n      <path d=\"M5.75 13.25h4.5\" />\n    </svg>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/sign-in-flow"
}
