{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auth-form",
  "title": "AuthForm",
  "description": "The email/password card: mode switching, providers, structured errors, and a working state — Better Auth-shaped.",
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/neon-tokens.json",
    "https://ui.neon.com/r/button.json"
  ],
  "files": [
    {
      "path": "src/components/auth-form/auth-form.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, FocusEvent, FormEvent, ReactNode } from \"react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AuthMode = \"sign-in\" | \"sign-up\" | \"reset\";\n\nexport type AuthFieldName = \"name\" | \"email\" | \"password\";\n\n/** Returns the failure message, or null when the value passes. */\nexport type AuthValidator = (value: string) => string | null;\n\nexport interface AuthProvider {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n}\n\nexport interface AuthFormValues {\n  email: string;\n  password: string;\n  /** Present in sign-up mode. */\n  name?: string;\n}\n\nexport type AuthFormProps = Omit<ComponentProps<\"form\">, \"onSubmit\"> & {\n  /** Which face the form shows. */\n  mode?: AuthMode;\n  /** Called with the field values once they pass validation. */\n  onSubmit: (values: AuthFormValues) => void;\n  /** Renders the footer swap link and enables mode switching. */\n  onModeChange?: (mode: AuthMode) => void;\n  /** Locks the fields and puts the action in its working state. */\n  isBusy?: boolean;\n  /** Structured failure message rendered above the action. */\n  error?: string | null;\n  /** Server-side verdicts pinned to fields (e.g. incorrect password). */\n  fieldErrors?: Partial<Record<AuthFieldName, string>>;\n  /**\n   * Per-field validators merged over the mode defaults — return the\n   * failure message, or null when the value passes. Use it to bring\n   * your own password policy.\n   */\n  validators?: Partial<Record<AuthFieldName, AuthValidator>>;\n  /** OAuth-style providers rendered under the divider. */\n  providers?: AuthProvider[];\n  /** Called with the provider id. */\n  onProvider?: (id: string) => void;\n  /** Renders the forgot link on the password row (sign-in only). */\n  onForgotPassword?: () => void;\n  /**\n   * The email a reset link was sent to. In reset mode this flips the\n   * form to its confirmation face.\n   */\n  resetSentTo?: string | null;\n  /** Renders the \"send again\" link on the confirmation face. */\n  onResend?: () => void;\n  /** Brand mark slot above the title. */\n  mark?: ReactNode;\n  title?: string;\n  description?: string;\n  /**\n   * \"card\" (default) draws the house surface — border, bg-card,\n   * padding. \"bare\" renders naked for split layouts that supply\n   * their own panel.\n   */\n  variant?: \"card\" | \"bare\";\n};\n\n/* ─────────────────────────────────────────────────────────\n * ENTRANCE STORYBOARD\n *\n * Read top-to-bottom. Each value is ms after mount; every\n * group rises 8px and fades in over 500ms, fields one at a\n * time — the form introduces itself in reading order.\n * Static under reduced motion.\n *\n *    0ms   mark, title, description\n *   80ms   first field (then +60ms per field)\n *  260ms   primary action\n *  340ms   divider, providers, footer\n * ───────────────────────────────────────────────────────── */\nconst TIMING: Record<\n  \"header\" | \"fields\" | \"fieldStagger\" | \"action\" | \"meta\",\n  number\n> = {\n  // primary action\n  action: 260,\n  // ms between each field\n  fieldStagger: 60,\n  // first field\n  fields: 80,\n  // mark, title, description\n  header: 0,\n  // divider, providers, footer\n  meta: 340,\n};\n\nconst RISE =\n  \"fill-mode-backwards fade-in-0 slide-in-from-bottom-2 animate-in duration-500 motion-reduce:animate-none\";\n\n/* ─────────────────────────────────────────────────────────\n * FIELD STORYBOARD — \"light in a tube\"\n *\n * The frame stays neutral; the light does the talking.\n *\n *  focus    a primary beam sweeps along the field's bottom\n *           edge, left to right (250ms strong ease-out,\n *           transform-only) and the label warms to\n *           foreground\n *  blur     the beam withdraws; a filled field validates on\n *           leave — never while you're still typing, and\n *           never for a field you merely tabbed past\n *           (required verdicts wait for submit)\n *  invalid  the whole box takes the verdict — destructive\n *           border and a whisper of destructive fill — the\n *           message takes over the label slot in place\n *           (crossfade, no layout shift — the frame never\n *           moves), and an X draws itself into the field\n *           edge\n *  valid    a check draws itself into the field edge,\n *           stroke first to last (300ms ease-out)\n *  edit     any verdict clears instantly — the system\n *           responds, it doesn't linger\n *  submit   all fields judged at once; the first failure\n *           takes focus; onSubmit fires only on a clean\n *           pass. Server fieldErrors pin until edited.\n * ───────────────────────────────────────────────────────── */\nconst EASE_OUT = \"cubic-bezier(0.23, 1, 0.32, 1)\";\n\nconst EMAIL_SHAPE = /^\\S+@\\S+\\.\\S+$/u;\n\n/* Better Auth's server defaults — client verdicts agree with them. */\nconst MIN_PASSWORD = 8;\nconst MAX_PASSWORD = 128;\n\nexport const validateEmail = (value: string): string | null => {\n  if (!value) {\n    return \"Add your email.\";\n  }\n\n  return EMAIL_SHAPE.test(value) ? null : \"Not a valid email.\";\n};\n\nconst VALIDATORS: Record<\n  AuthMode,\n  Partial<Record<AuthFieldName, (value: string) => string | null>>\n> = {\n  reset: {\n    email: validateEmail,\n  },\n  \"sign-in\": {\n    email: validateEmail,\n    password: (value) => (value ? null : \"Add your password.\"),\n  },\n  \"sign-up\": {\n    email: validateEmail,\n    name: (value) => (value.trim() ? null : \"Add your name.\"),\n    password: (value) => {\n      if (value.length < MIN_PASSWORD) {\n        return \"At least 8 characters.\";\n      }\n\n      return value.length > MAX_PASSWORD ? \"At most 128 characters.\" : null;\n    },\n  },\n};\n\n/* Per-mode copy: sentence case, no exclamation marks. */\nconst COPY: Record<\n  AuthMode,\n  { title: string; description: string; action: string; working: string }\n> = {\n  reset: {\n    action: \"Send reset link\",\n    description: \"Enter your email and we'll send you a reset link.\",\n    title: \"Reset your password\",\n    working: \"Sending…\",\n  },\n  \"sign-in\": {\n    action: \"Sign in\",\n    description: \"Sign in to continue to your workspace.\",\n    title: \"Welcome back\",\n    working: \"Signing in…\",\n  },\n  \"sign-up\": {\n    action: \"Create account\",\n    description: \"Start building on your own database.\",\n    title: \"Create your account\",\n    working: \"Creating account…\",\n  },\n};\n\n/* ─────────────────────────────────────────────────────────\n * ACTION STORYBOARD — \"ignition\"\n *\n *  dormant  ghost: hairline border, muted label — the form\n *           hasn't earned the color yet\n *  charged  every visible field has content: the action\n *           fills to neon on a 300ms ramp and a soft\n *           primary glow blooms\n *  press    scale 0.98, 160ms — the interface is listening\n *  busy     the working label shimmers under lock\n * ───────────────────────────────────────────────────────── */\n/* The charged CTA speaks through color and the sweep alone — no glow. */\nconst CTA_GLOW = \"\";\n\nconst AuthFormHeader = ({\n  description,\n  error,\n  mark,\n  title,\n}: {\n  description: string;\n  error: string | null;\n  mark?: ReactNode;\n  title: string;\n}) => (\n  <div\n    className={cn(\"flex flex-col gap-1.5\", RISE)}\n    style={{ animationDelay: `${TIMING.header}ms` }}\n  >\n    {mark ? (\n      <span className=\"mb-2 text-primary\" data-slot=\"auth-form-mark\">\n        {mark}\n      </span>\n    ) : null}\n    <h2 className=\"text-balance font-semibold text-foreground text-xl tracking-tight\">\n      {title}\n    </h2>\n    {/* The form's voice: normally the pitch, on failure the verdict —\n        swapped in place so the frame never moves. */}\n    <p\n      className={cn(\n        \"fade-in-0 animate-in text-pretty text-sm duration-200 motion-reduce:animate-none\",\n        error ? \"text-destructive\" : \"text-muted-foreground\"\n      )}\n      data-slot={error ? \"auth-form-error\" : undefined}\n      key={error ?? \"description\"}\n      role={error ? \"alert\" : undefined}\n    >\n      {error ?? description}\n    </p>\n  </div>\n);\n\n/** Divider, provider buttons, and the mode-swap footer. */\nconst FIELD_NAMES: Record<AuthMode, AuthFieldName[]> = {\n  reset: [\"email\"],\n  \"sign-in\": [\"email\", \"password\"],\n  \"sign-up\": [\"name\", \"email\", \"password\"],\n};\n\n/** Header copy resolution: overrides win, then the face speaks. */\nconst headerCopy = (\n  copy: (typeof COPY)[AuthMode],\n  sent: boolean,\n  resetSentTo: string | null,\n  title?: string,\n  description?: string\n) => ({\n  description:\n    description ??\n    (sent ? `A reset link is on its way to ${resetSentTo}.` : copy.description),\n  title: title ?? (sent ? \"Check your email\" : copy.title),\n});\n\n/** The confirmation face: a drawn check and the way back. */\nconst ResetSentFace = ({\n  isBusy,\n  onResend,\n}: {\n  isBusy: boolean;\n  onResend?: () => void;\n}) => (\n  <div\n    className={cn(\"flex flex-col items-center gap-4 py-2\", RISE)}\n    data-slot=\"auth-form-reset-sent\"\n    style={{ animationDelay: `${TIMING.fields}ms` }}\n  >\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-8 text-primary\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      strokeWidth=\"1.5\"\n      viewBox=\"0 0 24 24\"\n    >\n      <path\n        className=\"neon-check-draw\"\n        d=\"M4 12.5 10 18.5 20 6\"\n        pathLength={1}\n      />\n    </svg>\n    {onResend ? (\n      <button\n        className=\"text-muted-foreground text-xs underline-offset-4 transition-colors hover:text-foreground hover:underline\"\n        disabled={isBusy}\n        onClick={onResend}\n        type=\"button\"\n      >\n        send again\n      </button>\n    ) : null}\n  </div>\n);\n\n/* The footer swap per face: each mode offers the way back. */\nconst META_SWAP: Record<AuthMode, { ask: string; to: AuthMode; go: string }> = {\n  reset: { ask: \"Remembered it?\", go: \"Sign in\", to: \"sign-in\" },\n  \"sign-in\": { ask: \"New here?\", go: \"Create account\", to: \"sign-up\" },\n  \"sign-up\": { ask: \"Already have an account?\", go: \"Sign in\", to: \"sign-in\" },\n};\n\nconst AuthFormMeta = ({\n  isBusy,\n  mode,\n  onModeChange,\n  onProvider,\n  providers,\n}: {\n  isBusy: boolean;\n  mode: AuthMode;\n  onModeChange?: (mode: AuthMode) => void;\n  onProvider?: (id: string) => void;\n  providers?: AuthProvider[];\n}) => (\n  <div\n    className={cn(\"flex flex-col gap-4\", RISE)}\n    style={{ animationDelay: `${TIMING.meta}ms` }}\n  >\n    {providers?.length ? (\n      <>\n        <div\n          aria-hidden=\"true\"\n          className=\"flex items-center gap-3 text-muted-foreground/60\"\n        >\n          <span className=\"h-px flex-1 bg-border/60\" />\n          <span className=\"text-[10px]\">or continue with</span>\n          <span className=\"h-px flex-1 bg-border/60\" />\n        </div>\n        <div\n          className=\"grid auto-cols-fr grid-flow-col gap-2\"\n          data-slot=\"auth-form-providers\"\n        >\n          {providers.map((provider) => (\n            <Button\n              className=\"active:scale-[0.98]\"\n              disabled={isBusy}\n              key={provider.id}\n              onClick={() => onProvider?.(provider.id)}\n              type=\"button\"\n              variant=\"outline\"\n            >\n              {provider.icon}\n              <span className=\"text-xs\">{provider.label}</span>\n            </Button>\n          ))}\n        </div>\n      </>\n    ) : null}\n    {onModeChange ? (\n      <p className=\"text-center text-muted-foreground text-xs\">\n        {META_SWAP[mode].ask}{\" \"}\n        <button\n          className=\"text-foreground underline underline-offset-4 transition-colors hover:text-primary\"\n          onClick={() => onModeChange(META_SWAP[mode].to)}\n          type=\"button\"\n        >\n          {META_SWAP[mode].go}\n        </button>\n      </p>\n    ) : null}\n  </div>\n);\n\n/* ─────────────────────────────────────────────────────────\n * STRENGTH STORYBOARD — sign-up password only\n *\n * The beam doubles as the meter: its reach grows with the\n * password and its color warms from destructive through\n * neutral to primary. The word (\"weak\" / \"fair\" / \"strong\")\n * rides the trailing slot — nothing changes height.\n * Scoring matches Better Auth's defaults (8–128 chars) as\n * the floor, then rewards mixed case, digits, symbols, and\n * length.\n * ───────────────────────────────────────────────────────── */\nexport interface StrengthMeter {\n  label: string;\n  ratio: number;\n  tone: \"weak\" | \"fair\" | \"strong\";\n}\n\nconst METER_TONE: Record<StrengthMeter[\"tone\"], string> = {\n  fair: \"bg-foreground/50\",\n  strong: \"bg-primary\",\n  weak: \"bg-destructive\",\n};\n\nconst METER_WORD: Record<StrengthMeter[\"tone\"], string> = {\n  fair: \"text-muted-foreground\",\n  strong: \"text-primary\",\n  weak: \"text-destructive\",\n};\n\nconst STRENGTH_STEPS = 5;\n\nconst FAIR_FLOOR = 2;\nconst STRONG_FLOOR = 4;\nconst LONG_PASSWORD = 12;\n\nexport const scorePassword = (value: string): number => {\n  let score = 0;\n\n  if (value.length >= MIN_PASSWORD) {\n    score += 1;\n  }\n  if (value.length >= LONG_PASSWORD) {\n    score += 1;\n  }\n  if (/[a-z]/u.test(value) && /[A-Z]/u.test(value)) {\n    score += 1;\n  }\n  if (/\\d/u.test(value)) {\n    score += 1;\n  }\n  if (/[^a-zA-Z0-9]/u.test(value)) {\n    score += 1;\n  }\n\n  return score;\n};\n\nexport interface PasswordRequirement {\n  label: string;\n  met: boolean;\n}\n\n/** The checklist the popover renders while the password field is focused. */\nconst passwordRequirements = (value: string): PasswordRequirement[] => [\n  { label: \"8+ characters\", met: value.length >= MIN_PASSWORD },\n  {\n    label: \"upper & lower case\",\n    met: /[a-z]/u.test(value) && /[A-Z]/u.test(value),\n  },\n  { label: \"a number\", met: /\\d/u.test(value) },\n  { label: \"a symbol\", met: /[^a-zA-Z0-9]/u.test(value) },\n];\n\n/** Null when empty — the meter only speaks once you've started. */\nconst strengthMeter = (value: string): StrengthMeter | null => {\n  if (!value) {\n    return null;\n  }\n\n  const score = scorePassword(value);\n  let tone: StrengthMeter[\"tone\"] = \"weak\";\n\n  if (score >= STRONG_FLOOR) {\n    tone = \"strong\";\n  } else if (score >= FAIR_FLOOR) {\n    tone = \"fair\";\n  }\n\n  return {\n    label: tone,\n    ratio: tone === \"strong\" ? 1 : Math.max(score / STRENGTH_STEPS, 0.12),\n    tone,\n  };\n};\n\nconst beamTone = (\n  error: string | null | undefined,\n  meter: StrengthMeter | null | undefined\n) => {\n  // Invalid verdicts paint the whole box (see FIELD_INPUT usage);\n  // the beam stands down instead of doubling the signal.\n  if (error) {\n    return \"scale-x-0 bg-destructive\";\n  }\n  if (meter) {\n    return METER_TONE[meter.tone];\n  }\n\n  return \"scale-x-0 bg-primary group-focus-within:scale-x-100\";\n};\n\n/* ─────────────────────────────────────────────────────────\n * REQUIREMENTS POPOVER\n *\n * Portaled to the body and pinned to the input's rect (fixed\n * position, re-measured on scroll and resize), so it floats\n * above every sibling — no stacking context, not even the\n * charged CTA, can paint over it. Each rule flips\n * from a muted dot to a drawn primary check as the password\n * satisfies it. Floating, so nothing in the form shifts.\n * ───────────────────────────────────────────────────────── */\nconst RequirementsPopover = ({\n  anchor,\n  requirements,\n}: {\n  anchor: { current: HTMLInputElement | null };\n  requirements: PasswordRequirement[];\n}) => {\n  const [rect, setRect] = useState<DOMRect | null>(null);\n\n  useEffect(() => {\n    const measure = () => {\n      const el = anchor.current;\n\n      if (el) {\n        setRect(el.getBoundingClientRect());\n      }\n    };\n\n    const frame = requestAnimationFrame(measure);\n    window.addEventListener(\"scroll\", measure, true);\n    window.addEventListener(\"resize\", measure);\n    return () => {\n      cancelAnimationFrame(frame);\n      window.removeEventListener(\"scroll\", measure, true);\n      window.removeEventListener(\"resize\", measure);\n    };\n  }, [anchor]);\n\n  if (!rect) {\n    return null;\n  }\n\n  return createPortal(\n    <div\n      aria-hidden=\"true\"\n      className=\"fade-in-0 slide-in-from-bottom-1 pointer-events-none fixed z-50 flex w-max animate-in flex-col gap-1.5 rounded-md border border-border/60 bg-popover p-3 shadow-lg duration-200 motion-reduce:animate-none\"\n      data-slot=\"auth-form-requirements\"\n      style={{ left: rect.left, top: rect.bottom + 8 }}\n    >\n      {requirements.map((rule) => (\n        <span\n          className={cn(\n            \"flex items-center gap-2 text-xs transition-colors duration-200\",\n            rule.met ? \"text-foreground\" : \"text-muted-foreground/70\"\n          )}\n          data-met={rule.met || undefined}\n          key={rule.label}\n        >\n          {rule.met ? (\n            <svg\n              className=\"size-3 text-primary\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth=\"2.5\"\n              viewBox=\"0 0 24 24\"\n            >\n              <path\n                className=\"neon-check-draw\"\n                d=\"M4 12.5 10 18.5 20 6\"\n                pathLength={1}\n              />\n            </svg>\n          ) : (\n            <span className=\"mx-[5px] size-0.5 rounded-full bg-muted-foreground/70\" />\n          )}\n          {rule.label}\n        </span>\n      ))}\n    </div>,\n    document.body\n  );\n};\n\n/* The field frame stays neutral in every state — the beam under the\n * input and the label carry the verdict. */\nconst FIELD_INPUT =\n  \"w-full rounded-md border border-border/60 bg-transparent px-3 py-2 text-base caret-primary sm:text-sm outline-none transition-colors placeholder:text-muted-foreground/60 hover:border-border focus:border-border disabled:cursor-not-allowed disabled:opacity-60\";\n\n/** A verdict glyph that draws itself in, stroke first to last. */\nconst DrawnGlyph = ({ kind }: { kind: \"check\" | \"cross\" }) => (\n  <svg\n    aria-hidden=\"true\"\n    className={cn(\n      \"-translate-y-1/2 absolute top-1/2 right-3 size-3.5\",\n      kind === \"check\" ? \"text-primary\" : \"text-destructive\"\n    )}\n    data-slot={`auth-form-field-${kind}`}\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    strokeWidth=\"2\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      className=\"neon-check-draw\"\n      d={kind === \"check\" ? \"M4 12.5 10 18.5 20 6\" : \"m6 6 12 12m0-12L6 18\"}\n      pathLength={1}\n    />\n  </svg>\n);\n\n/** One field: mono label slot that the verdict takes over in place,\n * a focus beam, and a drawn check or cross at the field edge. */\nconst AuthField = ({\n  autoComplete,\n  delay,\n  disabled,\n  error,\n  label,\n  meter,\n  name,\n  onEdit,\n  requirements,\n  onLeave,\n  placeholder,\n  trailing,\n  type,\n  valid,\n}: {\n  autoComplete: string;\n  delay: number;\n  disabled: boolean;\n  error?: string | null;\n  label: string;\n  meter?: StrengthMeter | null;\n  name: AuthFieldName;\n  requirements?: PasswordRequirement[];\n  onEdit: (field: AuthFieldName) => void;\n  onLeave: (field: AuthFieldName, value: string) => void;\n  placeholder: string;\n  trailing?: ReactNode;\n  type: string;\n  valid?: boolean;\n}) => {\n  const messageId = useId();\n  const anchorRef = useRef<HTMLInputElement>(null);\n  const [focused, setFocused] = useState(false);\n\n  return (\n    <label\n      className={cn(\"group flex flex-col gap-1.5\", RISE)}\n      data-invalid={error ? true : undefined}\n      data-slot=\"auth-form-field\"\n      style={{ animationDelay: `${delay}ms` }}\n    >\n      {/* The label slot: the verdict takes it over in place — same\n          row, same height, zero layout shift. */}\n      <span className=\"flex items-baseline justify-between\">\n        <span\n          className={cn(\n            \"fade-in-0 min-w-0 flex-1 animate-in truncate text-xs duration-200 motion-reduce:animate-none\",\n            error\n              ? \"text-destructive\"\n              : \"text-muted-foreground transition-colors group-focus-within:text-foreground\"\n          )}\n          id={messageId}\n          key={error ?? label}\n          role={error ? \"alert\" : undefined}\n        >\n          {error ?? label}\n        </span>\n        {trailing}\n      </span>\n      <span className=\"relative\">\n        <input\n          aria-describedby={error ? messageId : undefined}\n          aria-invalid={error ? true : undefined}\n          autoComplete={autoComplete}\n          className={cn(\n            FIELD_INPUT,\n            (valid || error) && \"pr-9\",\n            // An invalid verdict tints the whole box, not just an\n            // underline — border and a whisper of fill.\n            error &&\n              \"border-destructive/60 bg-destructive/[0.04] hover:border-destructive/70 focus:border-destructive/70\"\n          )}\n          disabled={disabled}\n          name={name}\n          onBlur={(event: FocusEvent<HTMLInputElement>) => {\n            setFocused(false);\n            onLeave(name, event.target.value);\n          }}\n          onChange={() => onEdit(name)}\n          onFocus={() => setFocused(true)}\n          placeholder={placeholder}\n          ref={anchorRef}\n          type={type}\n        />\n        {/* The beam: primary light sweeps in on focus; a verdict\n            relights it in destructive and keeps it lit. With a\n            strength meter, the beam's reach IS the reading. */}\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"pointer-events-none absolute inset-x-px bottom-0 h-[1.5px] origin-left motion-reduce:transition-none\",\n            beamTone(error, meter)\n          )}\n          data-slot=\"auth-form-field-beam\"\n          style={{\n            transition: `scale 250ms ${EASE_OUT}, background-color 250ms ease`,\n            ...(meter && !error && { scale: `${meter.ratio} 1` }),\n          }}\n        />\n        {valid ? <DrawnGlyph kind=\"check\" /> : null}\n        {error ? <DrawnGlyph kind=\"cross\" /> : null}\n        {requirements && focused ? (\n          <RequirementsPopover anchor={anchorRef} requirements={requirements} />\n        ) : null}\n      </span>\n    </label>\n  );\n};\n\n/** Sign up shows the strength reading; sign in shows the forgot link. */\nconst passwordTrailing = (\n  signUp: boolean,\n  meter: StrengthMeter | null,\n  onForgotPassword?: () => void\n): ReactNode => {\n  if (signUp) {\n    return meter ? (\n      <span\n        aria-live=\"polite\"\n        className={cn(\n          \"fade-in-0 animate-in font-mono text-xs duration-200 motion-reduce:animate-none\",\n          METER_WORD[meter.tone]\n        )}\n        data-slot=\"auth-form-strength\"\n        key={meter.tone}\n      >\n        {meter.label}\n      </span>\n    ) : undefined;\n  }\n\n  return onForgotPassword ? (\n    <button\n      className=\"text-muted-foreground text-xs underline-offset-4 transition-colors hover:text-foreground hover:underline\"\n      onClick={onForgotPassword}\n      type=\"button\"\n    >\n      Forgot password?\n    </button>\n  ) : undefined;\n};\n\n/** The per-mode field stack; hidden (not unmounted) on the sent face. */\nconst AuthFormFields = ({\n  hidden,\n  isBusy,\n  meter,\n  mode,\n  onEdit,\n  onForgotPassword,\n  onLeave,\n  requirements,\n  verdictFor,\n}: {\n  hidden: boolean;\n  isBusy: boolean;\n  meter: StrengthMeter | null;\n  mode: AuthMode;\n  onEdit: (field: AuthFieldName) => void;\n  onForgotPassword?: () => void;\n  onLeave: (field: AuthFieldName, value: string) => void;\n  requirements?: PasswordRequirement[];\n  verdictFor: (field: AuthFieldName) => {\n    error: string | null;\n    valid: boolean;\n  };\n}) => {\n  const signUp = mode === \"sign-up\";\n  const reset = mode === \"reset\";\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-4\", hidden && \"hidden\")}\n      key={`${mode}-${String(hidden)}`}\n    >\n      {signUp ? (\n        <AuthField\n          autoComplete=\"name\"\n          delay={TIMING.fields}\n          disabled={isBusy}\n          label=\"name\"\n          name=\"name\"\n          onEdit={onEdit}\n          onLeave={onLeave}\n          placeholder=\"Ada Lovelace\"\n          type=\"text\"\n          {...verdictFor(\"name\")}\n        />\n      ) : null}\n      <AuthField\n        autoComplete=\"email\"\n        delay={TIMING.fields + (signUp ? TIMING.fieldStagger : 0)}\n        disabled={isBusy}\n        label=\"email\"\n        name=\"email\"\n        onEdit={onEdit}\n        onLeave={onLeave}\n        placeholder=\"you@example.com\"\n        type=\"email\"\n        {...verdictFor(\"email\")}\n      />\n      {reset ? null : (\n        <AuthField\n          autoComplete={signUp ? \"new-password\" : \"current-password\"}\n          delay={TIMING.fields + TIMING.fieldStagger * (signUp ? 2 : 1)}\n          disabled={isBusy}\n          label=\"password\"\n          meter={meter}\n          name=\"password\"\n          onEdit={onEdit}\n          onLeave={onLeave}\n          placeholder=\"••••••••\"\n          requirements={requirements}\n          trailing={passwordTrailing(signUp, meter, onForgotPassword)}\n          type=\"password\"\n          {...verdictFor(\"password\")}\n        />\n      )}\n    </div>\n  );\n};\n\ntype Verdicts = Partial<\n  Record<AuthFieldName, { error: string | null; valid: boolean }>\n>;\n\n/** Server verdicts pin first; local blur/submit verdicts follow. */\nconst resolveVerdict = (\n  field: AuthFieldName,\n  verdicts: Verdicts,\n  fieldErrors?: Partial<Record<AuthFieldName, string>>\n) => {\n  const server = fieldErrors?.[field];\n  const local = verdicts[field];\n  return {\n    error: server ?? local?.error ?? null,\n    valid: !server && local?.valid === true,\n  };\n};\n\n/** The ignition action. */\nconst AuthFormAction = ({\n  charged,\n  isBusy,\n  label,\n  working,\n}: {\n  charged: boolean;\n  isBusy: boolean;\n  label: string;\n  working: string;\n}) => (\n  <div\n    className={cn(\"flex flex-col gap-3\", RISE)}\n    style={{ animationDelay: `${TIMING.action}ms` }}\n  >\n    <Button\n      className={cn(\n        \"w-full transition-[background-color,color,border-color,box-shadow,scale] duration-300 active:scale-[0.98] motion-reduce:active:scale-100\",\n        charged || isBusy\n          ? cn(CTA_GLOW, \"disabled:opacity-100\")\n          : \"border border-border/60 bg-transparent text-muted-foreground hover:bg-transparent hover:text-foreground\"\n      )}\n      data-charged={charged || undefined}\n      disabled={isBusy}\n      type=\"submit\"\n    >\n      {/* animate-in and shimmer both own the animation shorthand, so\n          the shimmer rides an inner span. */}\n      <span className=\"fade-in-0 animate-in duration-300\" key={String(isBusy)}>\n        <span\n          className={cn(\"block\", { \"shimmer shimmer-duration-2400\": isBusy })}\n        >\n          {isBusy ? working : label}\n        </span>\n      </span>\n    </Button>\n  </div>\n);\n\n/** Judge every visible field at once; report the first failure. */\nconst judgeAll = (\n  rules: Partial<Record<AuthFieldName, AuthValidator>>,\n  fields: AuthFieldName[],\n  data: FormData\n) => {\n  const verdicts: Verdicts = {};\n  let firstFailure: AuthFieldName | null = null;\n\n  for (const field of fields) {\n    const check = rules[field];\n\n    if (!check) {\n      continue;\n    }\n\n    const value = String(data.get(field) ?? \"\");\n    const failure = check(value);\n    verdicts[field] = { error: failure, valid: !failure && value.length > 0 };\n\n    if (failure && !firstFailure) {\n      firstFailure = field;\n    }\n  }\n\n  return { firstFailure, verdicts };\n};\n\nexport const AuthForm = ({\n  className,\n  description,\n  error = null,\n  fieldErrors,\n  isBusy = false,\n  mark,\n  mode = \"sign-in\",\n  onForgotPassword,\n  onModeChange,\n  onResend,\n  onProvider,\n  onSubmit,\n  providers,\n  resetSentTo = null,\n  title,\n  validators,\n  variant = \"card\",\n  ...props\n}: AuthFormProps) => {\n  const copy = COPY[mode];\n  const signUp = mode === \"sign-up\";\n  const reset = mode === \"reset\";\n  const sent = reset && Boolean(resetSentTo);\n  const rules = { ...VALIDATORS[mode], ...validators };\n  const [judged, setJudged] = useState<{ mode: AuthMode; verdicts: Verdicts }>({\n    mode,\n    verdicts: {},\n  });\n  const [charged, setCharged] = useState(false);\n  const [password, setPassword] = useState(\"\");\n\n  // Render-time re-seed: flipping modes clears every verdict.\n  if (judged.mode !== mode) {\n    setJudged({ mode, verdicts: {} });\n    setCharged(false);\n    setPassword(\"\");\n  }\n\n  const fieldNames = FIELD_NAMES[mode];\n\n  const verdictFor = (field: AuthFieldName) =>\n    resolveVerdict(field, judged.verdicts, fieldErrors);\n\n  const judge = (field: AuthFieldName, value: string) => {\n    const check = rules[field];\n\n    // Skipping past an empty field isn't a mistake yet — required\n    // verdicts wait for submit.\n    if (!(check && value)) {\n      return;\n    }\n\n    const failure = check(value);\n    setJudged((prev) => ({\n      mode,\n      verdicts: {\n        ...prev.verdicts,\n        [field]: { error: failure, valid: !failure && value.length > 0 },\n      },\n    }));\n  };\n\n  const clear = (field: AuthFieldName) => {\n    setJudged((prev) => ({\n      mode,\n      verdicts: { ...prev.verdicts, [field]: undefined },\n    }));\n  };\n\n  /** The action charges once every visible field has content. */\n  const handleFormChange = (event: FormEvent<HTMLFormElement>) => {\n    const data = new FormData(event.currentTarget);\n    setCharged(\n      fieldNames.every((field) => String(data.get(field) ?? \"\").length > 0)\n    );\n    setPassword(String(data.get(\"password\") ?? \"\"));\n  };\n\n  const meter = signUp ? strengthMeter(password) : null;\n  const requirements = signUp ? passwordRequirements(password) : undefined;\n\n  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n\n    if (isBusy) {\n      return;\n    }\n\n    const form = event.currentTarget;\n    const data = new FormData(form);\n    const values: AuthFormValues = {\n      email: String(data.get(\"email\") ?? \"\"),\n      password: String(data.get(\"password\") ?? \"\"),\n      ...(signUp && { name: String(data.get(\"name\") ?? \"\") }),\n    };\n\n    if (sent) {\n      return;\n    }\n\n    // Judge everything at once; the first failure takes focus.\n    const { firstFailure, verdicts } = judgeAll(rules, fieldNames, data);\n    setJudged({ mode, verdicts });\n\n    if (firstFailure) {\n      form.querySelector<HTMLInputElement>(`[name=\"${firstFailure}\"]`)?.focus();\n      return;\n    }\n\n    onSubmit(values);\n  };\n\n  return (\n    <form\n      className={cn(\n        \"flex w-full max-w-sm flex-col gap-6\",\n        variant === \"card\" &&\n          \"rounded-lg border border-border/60 bg-card p-6 transition-colors hover:border-border sm:p-8\",\n        className\n      )}\n      data-busy={isBusy || undefined}\n      data-mode={mode}\n      data-slot=\"auth-form\"\n      noValidate\n      onChange={handleFormChange}\n      onSubmit={handleSubmit}\n      {...props}\n    >\n      <AuthFormHeader\n        error={error}\n        mark={mark}\n        {...headerCopy(copy, sent, resetSentTo, title, description)}\n      />\n\n      {/* The confirmation face: the fields yield — the form's work is\n          done, the inbox's begins. */}\n      {sent ? <ResetSentFace isBusy={isBusy} onResend={onResend} /> : null}\n\n      {/* Fields crossfade when the mode flips, entering one at a time. */}\n      <AuthFormFields\n        hidden={sent}\n        isBusy={isBusy}\n        meter={meter}\n        mode={mode}\n        onEdit={clear}\n        onForgotPassword={onForgotPassword}\n        onLeave={judge}\n        requirements={requirements}\n        verdictFor={verdictFor}\n      />\n\n      {sent ? null : (\n        <AuthFormAction\n          charged={charged}\n          isBusy={isBusy}\n          label={copy.action}\n          working={copy.working}\n        />\n      )}\n\n      {(providers?.length || onModeChange) && (\n        <AuthFormMeta\n          isBusy={isBusy}\n          mode={mode}\n          onModeChange={onModeChange}\n          onProvider={onProvider}\n          providers={reset ? undefined : providers}\n        />\n      )}\n    </form>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}