{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "email-otp",
  "title": "Email OTP",
  "description": "Email OTP plugin: passwordless sign-in with an emailed code, plus code-based replacements for email verification, password reset, and email changes.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "better-auth",
    "input-otp",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "field",
    "https://better-auth-ui.com/r/radix-nova/account-settings.json",
    "input",
    "input-group",
    "input-otp",
    "skeleton",
    "sonner",
    "spinner",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/lib/auth/auth-plugin.ts",
      "content": "import type {\n  AuthPluginComponents,\n  AuthPlugin as AuthPluginPrimitive\n} from \"@better-auth-ui/react\"\n\ndeclare module \"@better-auth-ui/core\" {\n  /** Widens `useAuth().plugins` to the shadcn-typed `AuthPlugin`. */\n  interface AuthPluginRegister {\n    shadcn: AuthPlugin\n  }\n}\n\n/** Props the shadcn `<Auth>` router spreads onto plugin-contributed auth views. */\nexport type AuthViewProps = {\n  className?: string\n  socialLayout?: \"auto\" | \"horizontal\" | \"vertical\" | \"grid\"\n  socialPosition?: \"top\" | \"bottom\"\n}\n\n/** Props the shadcn `<Settings>` router spreads onto plugin-contributed settings views. */\nexport type SettingsViewProps = {\n  className?: string\n}\n\n/** Shadcn plugin type. Plugin authors import this from `@/lib/auth/auth-plugin`. */\nexport type AuthPlugin = AuthPluginPrimitive<\n  AuthPluginComponents,\n  AuthViewProps,\n  SettingsViewProps\n>\n",
      "type": "registry:lib",
      "target": "@lib/auth/auth-plugin.ts"
    },
    {
      "path": "src/lib/auth/email-otp-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  emailOtpPlugin as coreEmailOtpPlugin,\n  type EmailOtpPluginOptions\n} from \"@better-auth-ui/core/plugins\"\n\nimport { ChangeEmailOtp } from \"@/components/auth/email-otp/change-email-otp\"\nimport { EmailOtp } from \"@/components/auth/email-otp/email-otp\"\nimport { EmailOtpButton } from \"@/components/auth/email-otp/email-otp-button\"\nimport { ForgotPasswordOtp } from \"@/components/auth/email-otp/forgot-password-otp\"\nimport { ResetPasswordOtp } from \"@/components/auth/email-otp/reset-password-otp\"\nimport { VerifyEmailOtp } from \"@/components/auth/email-otp/verify-email-otp\"\n\nexport const emailOtpPlugin = createAuthPlugin(\n  coreEmailOtpPlugin.id,\n  (options: EmailOtpPluginOptions = {}) => {\n    const plugin = coreEmailOtpPlugin(options)\n\n    return {\n      ...plugin,\n      authButtons: plugin.signIn ? [EmailOtpButton] : [],\n      // Each flow is opt-in because it replaces a link-based view outright.\n      // Turning one on without the matching server option would leave the\n      // user waiting for a code that never arrives.\n      views: {\n        auth: {\n          ...(plugin.signIn && { emailOtp: EmailOtp }),\n          ...(plugin.emailVerification && { verifyEmail: VerifyEmailOtp }),\n          ...(plugin.passwordReset && {\n            forgotPassword: ForgotPasswordOtp,\n            resetPassword: ResetPasswordOtp\n          })\n        }\n      },\n      // Conditional, not an override: when `emailAndPassword.enabled === false`\n      // the `<Auth>` router renders this at `/auth/sign-in` instead of the\n      // disabled password form.\n      ...(plugin.signIn && {\n        fallbackViews: { auth: { signIn: EmailOtp } }\n      }),\n      ...(plugin.changeEmail && {\n        cardOverrides: { account: { changeEmail: ChangeEmailOtp } }\n      })\n    }\n  }\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/email-otp-plugin.ts"
    },
    {
      "path": "src/lib/auth/use-resend-cooldown.ts",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useState } from \"react\"\n\n/** Seconds a resend button stays disabled to keep users off the rate limit. */\nexport const RESEND_COOLDOWN_SECONDS = 60\n\n/**\n * Countdown state for \"resend code\" buttons.\n *\n * @param initialSeconds - Seconds to start at. Pass `0` when nothing has been\n *   sent yet, or the cooldown length when the flow arrives right after a send.\n */\nexport function useResendCooldown(initialSeconds = 0) {\n  const [cooldown, setCooldown] = useState(initialSeconds)\n\n  useEffect(() => {\n    if (cooldown <= 0) return\n\n    const interval = setInterval(() => {\n      setCooldown((current) => (current > 0 ? current - 1 : 0))\n    }, 1000)\n\n    return () => clearInterval(interval)\n  }, [cooldown])\n\n  // Stable so callers can start the cooldown from an effect without\n  // re-running it on every render.\n  const startCooldown = useCallback(\n    (seconds = RESEND_COOLDOWN_SECONDS) => setCooldown(seconds),\n    []\n  )\n\n  return {\n    cooldown,\n    isCoolingDown: cooldown > 0,\n    startCooldown\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/auth/use-resend-cooldown.ts"
    },
    {
      "path": "src/lib/auth/use-sign-in-continuation.ts",
      "content": "\"use client\"\n\nimport { useAuth } from \"@better-auth-ui/react\"\nimport { useCallback } from \"react\"\nimport {\n  isTwoFactorRedirect,\n  storeTwoFactorMethods,\n  TWO_FACTOR_PLUGIN_ID\n} from \"./two-factor-methods\"\n\n/**\n * Resolve what happens after a sign-in request succeeds.\n *\n * Better Auth withholds the session when a second factor is required and\n * answers with `{ twoFactorRedirect: true, twoFactorMethods }` instead, so no\n * sign-in strategy may navigate to `redirectTo` unconditionally. This hook is\n * the single place that decision lives — every password-based form calls it\n * from `onSuccess`.\n *\n * The enabled methods are stashed in session storage (names only, never a\n * code or token) and `redirectTo` rides along in the query string so the\n * challenge view can finish the original navigation.\n *\n * The two-factor plugin is looked up by its stable id, so sign-in forms stay\n * installable without either the two-factor components or a matching release\n * of `@better-auth-ui/core`.\n *\n * @returns A callback taking the resolved data of a sign-in mutation.\n */\nexport function useSignInContinuation() {\n  const { basePaths, navigate, plugins, redirectTo } = useAuth()\n\n  const twoFactorPath = plugins.find(\n    (plugin) => plugin.id === TWO_FACTOR_PLUGIN_ID\n  )?.viewPaths?.auth?.twoFactor\n\n  return useCallback(\n    (data: unknown) => {\n      if (twoFactorPath && isTwoFactorRedirect(data)) {\n        storeTwoFactorMethods(data.twoFactorMethods)\n\n        navigate({\n          to: `${basePaths.auth}/${twoFactorPath}?redirectTo=${encodeURIComponent(redirectTo)}`\n        })\n        return\n      }\n\n      navigate({ to: redirectTo })\n    },\n    [basePaths.auth, navigate, redirectTo, twoFactorPath]\n  )\n}\n",
      "type": "registry:lib",
      "target": "@lib/auth/use-sign-in-continuation.ts"
    },
    {
      "path": "src/lib/auth/two-factor-methods.ts",
      "content": "/**\n * Second-factor methods a user can complete the challenge with.\n *\n * `totp` and `otp` come from the server's `twoFactorMethods` list. Backup\n * codes are always available when the server has them enabled, so they are\n * never part of that list.\n */\nexport type TwoFactorMethod = \"totp\" | \"otp\"\n\nconst TWO_FACTOR_METHODS: TwoFactorMethod[] = [\"totp\", \"otp\"]\n\n/** Auth plugin id used by Better Auth UI's two-factor integration. */\nexport const TWO_FACTOR_PLUGIN_ID = \"twoFactor\"\n\n/**\n * `sessionStorage` key holding the methods reported by the sign-in response.\n *\n * Only the non-sensitive method names are stored, never a code, token, or the\n * two-factor cookie, which stays HTTP-only.\n */\nexport const TWO_FACTOR_METHODS_STORAGE_KEY =\n  \"better-auth-ui.two-factor-methods\"\n\ntype TwoFactorRedirect = {\n  twoFactorRedirect: true\n  twoFactorMethods?: unknown\n}\n\n/** Detect the redirect payload Better Auth returns before a second factor. */\nexport function isTwoFactorRedirect(data: unknown): data is TwoFactorRedirect {\n  return (\n    typeof data === \"object\" &&\n    data !== null &&\n    (data as { twoFactorRedirect?: unknown }).twoFactorRedirect === true\n  )\n}\n\n/** Narrow arbitrary method names to the challenge views this UI supports. */\nexport function parseTwoFactorMethods(methods?: unknown): TwoFactorMethod[] {\n  if (!Array.isArray(methods)) return []\n\n  return TWO_FACTOR_METHODS.filter((method) => methods.includes(method))\n}\n\n/** Persist the enabled method names without blocking sign-in on storage errors. */\nexport function storeTwoFactorMethods(methods?: unknown) {\n  if (typeof sessionStorage === \"undefined\") return\n\n  try {\n    sessionStorage.setItem(\n      TWO_FACTOR_METHODS_STORAGE_KEY,\n      JSON.stringify(parseTwoFactorMethods(methods))\n    )\n  } catch {\n    // The challenge falls back to every method when storage is unavailable.\n  }\n}\n\n/** Read the stored methods, falling back to every supported challenge. */\nexport function readTwoFactorMethods(): TwoFactorMethod[] {\n  if (typeof sessionStorage === \"undefined\") return TWO_FACTOR_METHODS\n\n  try {\n    const stored = sessionStorage.getItem(TWO_FACTOR_METHODS_STORAGE_KEY)\n    if (!stored) return TWO_FACTOR_METHODS\n\n    const methods = parseTwoFactorMethods(JSON.parse(stored))\n    return methods.length ? methods : TWO_FACTOR_METHODS\n  } catch {\n    return TWO_FACTOR_METHODS\n  }\n}\n\n/** Clear stored method hints after the challenge finishes or is abandoned. */\nexport function clearTwoFactorMethods() {\n  if (typeof sessionStorage === \"undefined\") return\n\n  try {\n    sessionStorage.removeItem(TWO_FACTOR_METHODS_STORAGE_KEY)\n  } catch {\n    // Stale method hints are harmless and must not block navigation.\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/auth/two-factor-methods.ts"
    },
    {
      "path": "src/components/auth/otp-field.tsx",
      "content": "\"use client\"\n\nimport { REGEXP_ONLY_DIGITS } from \"input-otp\"\nimport { useId } from \"react\"\n\nimport { Field, FieldError, FieldLabel } from \"@/components/ui/field\"\nimport {\n  InputOTP,\n  InputOTPGroup,\n  InputOTPSlot\n} from \"@/components/ui/input-otp\"\nimport { cn } from \"@/lib/utils\"\n\nexport type OtpFieldProps = {\n  /** Visible label rendered above the slots. */\n  label: string\n  /** Number of slots — keep in sync with the server's code length. */\n  length: number\n  value: string\n  onChange: (value: string) => void\n  onComplete?: (value: string) => void\n  autoFocus?: boolean\n  className?: string\n  errorMessage?: string\n  disabled?: boolean\n  name?: string\n}\n\n/** Strip everything the numeric slots can't hold — pasted codes often carry spaces or dashes. */\nfunction normalizeCode(value: string) {\n  return value.replace(/\\D/g, \"\")\n}\n\n/**\n * Labelled one-time-code input.\n *\n * Shared by every code-based flow (email OTP, two-factor challenge,\n * two-factor enrollment) so slot sizing, pasting, and error wiring behave the\n * same everywhere.\n *\n * @param label - Visible label, also used as the accessible name.\n * @param length - Number of code characters.\n * @param errorMessage - Rendered below the slots when set.\n */\nexport function OtpField({\n  autoFocus,\n  className,\n  disabled,\n  errorMessage,\n  label,\n  length,\n  name,\n  onChange,\n  onComplete,\n  value\n}: OtpFieldProps) {\n  const inputId = useId()\n\n  return (\n    <Field className={cn(className)} data-invalid={!!errorMessage}>\n      <FieldLabel htmlFor={inputId}>{label}</FieldLabel>\n\n      <InputOTP\n        aria-invalid={!!errorMessage}\n        aria-label={label}\n        autoComplete=\"one-time-code\"\n        autoFocus={autoFocus}\n        containerClassName=\"w-full justify-center\"\n        disabled={disabled}\n        id={inputId}\n        inputMode=\"numeric\"\n        maxLength={length}\n        name={name}\n        pasteTransformer={normalizeCode}\n        pattern={REGEXP_ONLY_DIGITS}\n        value={value}\n        onChange={(next) => onChange(normalizeCode(next))}\n        onComplete={(completedCode) =>\n          onComplete?.(normalizeCode(completedCode))\n        }\n      >\n        <InputOTPGroup>\n          {Array.from({ length }, (_, slotIndex) => (\n            <InputOTPSlot\n              index={slotIndex}\n              key={`otp-slot-${String(slotIndex + 1)}`}\n            />\n          ))}\n        </InputOTPGroup>\n      </InputOTP>\n\n      <FieldError>{errorMessage}</FieldError>\n    </Field>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/otp-field.tsx"
    },
    {
      "path": "src/components/auth/email-otp/email-otp.tsx",
      "content": "\"use client\"\n\nimport { authMutationKeys } from \"@better-auth-ui/core\"\nimport {\n  type EmailOtpAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useSendVerificationOtp,\n  useSignInEmailOtp\n} from \"@better-auth-ui/react\"\nimport { useIsMutating } from \"@tanstack/react-query\"\nimport { type SyntheticEvent, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport {\n  Field,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel,\n  FieldSeparator\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { emailOtpPlugin } from \"@/lib/auth/email-otp-plugin\"\nimport { useResendCooldown } from \"@/lib/auth/use-resend-cooldown\"\nimport { useSignInContinuation } from \"@/lib/auth/use-sign-in-continuation\"\nimport { cn } from \"@/lib/utils\"\nimport { OpenEmailButton } from \"../open-email-button\"\nimport { OtpField } from \"../otp-field\"\nimport { ProviderButtons, type SocialLayout } from \"../provider-buttons\"\n\nexport type EmailOtpProps = {\n  className?: string\n  socialLayout?: SocialLayout\n  socialPosition?: \"top\" | \"bottom\"\n}\n\n/**\n * Passwordless sign-in with an emailed one-time code.\n *\n * Two steps on one route: enter an email, then enter the code that arrives.\n * The email step never reveals whether an account exists — the server decides\n * whether the code creates an account, mirroring `emailOTP({ disableSignUp })`.\n *\n * @param socialLayout - Provider button layout.\n * @param socialPosition - `\"top\"` or `\"bottom\"`. Defaults to `\"bottom\"`.\n */\nexport function EmailOtp({\n  className,\n  socialLayout,\n  socialPosition = \"bottom\"\n}: EmailOtpProps) {\n  const {\n    authClient,\n    basePaths,\n    emailAndPassword,\n    localization,\n    plugins,\n    socialProviders,\n    viewPaths,\n    Link\n  } = useAuth()\n  const { localization: emailOtpLocalization, otpLength } =\n    useAuthPlugin(emailOtpPlugin)\n\n  const otpClient = authClient as EmailOtpAuthClient\n  const continueSignIn = useSignInContinuation()\n  const { cooldown, isCoolingDown, startCooldown } = useResendCooldown()\n\n  const [email, setEmail] = useState(\"\")\n  const [code, setCode] = useState(\"\")\n  const [codeSent, setCodeSent] = useState(false)\n  const [fieldErrors, setFieldErrors] = useState<{ email?: string }>({})\n\n  const { mutate: sendVerificationOtp, isPending: isSending } =\n    useSendVerificationOtp(otpClient, {\n      onSuccess: () => {\n        setCodeSent(true)\n        startCooldown()\n      }\n    })\n\n  const { mutate: signInEmailOtp, isPending: isSigningIn } = useSignInEmailOtp(\n    otpClient,\n    {\n      onError: () => setCode(\"\"),\n      onSuccess: (data) => continueSignIn(data)\n    }\n  )\n\n  const signInMutating = useIsMutating({\n    mutationKey: authMutationKeys.signIn.all\n  })\n  const signUpMutating = useIsMutating({\n    mutationKey: authMutationKeys.signUp.all\n  })\n  const isPending = signInMutating + signUpMutating > 0 || isSending\n\n  const sendCode = () => sendVerificationOtp({ email, type: \"sign-in\" })\n  const verifyCode = (completedCode: string) => {\n    if (isPending || isSigningIn) return\n\n    signInEmailOtp({ email, otp: completedCode })\n  }\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    if (!codeSent) {\n      sendCode()\n      return\n    }\n\n    verifyCode(code)\n  }\n\n  const showSeparator = socialProviders && socialProviders.length > 0\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">{localization.auth.signIn}</CardTitle>\n\n        {codeSent && (\n          <CardDescription>\n            {emailOtpLocalization.codeSentTo.replace(\"{{email}}\", email)}\n          </CardDescription>\n        )}\n      </CardHeader>\n\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {socialPosition === \"top\" && !codeSent && (\n            <>\n              {socialProviders && socialProviders.length > 0 && (\n                <ProviderButtons socialLayout={socialLayout} view=\"emailOtp\" />\n              )}\n\n              {showSeparator && (\n                <FieldSeparator className=\"*:data-[slot=field-separator-content]:bg-card m-0 text-xs flex items-center\">\n                  {localization.auth.or}\n                </FieldSeparator>\n              )}\n            </>\n          )}\n\n          <form onSubmit={handleSubmit}>\n            <FieldGroup>\n              {codeSent ? (\n                <OtpField\n                  autoFocus\n                  disabled={isPending || isSigningIn}\n                  label={emailOtpLocalization.code}\n                  length={otpLength}\n                  name=\"otp\"\n                  value={code}\n                  onChange={setCode}\n                  onComplete={verifyCode}\n                />\n              ) : (\n                <Field data-invalid={!!fieldErrors.email}>\n                  <FieldLabel htmlFor=\"email\">\n                    {localization.auth.email}\n                  </FieldLabel>\n\n                  <Input\n                    id=\"email\"\n                    name=\"email\"\n                    type=\"email\"\n                    autoComplete=\"email\"\n                    value={email}\n                    onChange={(e) => {\n                      setEmail(e.target.value)\n                      setFieldErrors((prev) => ({ ...prev, email: undefined }))\n                    }}\n                    placeholder={localization.auth.emailPlaceholder}\n                    required\n                    disabled={isPending}\n                    onInvalid={(e) => {\n                      e.preventDefault()\n\n                      setFieldErrors((prev) => ({\n                        ...prev,\n                        email: (e.target as HTMLInputElement).validationMessage\n                      }))\n                    }}\n                    aria-invalid={!!fieldErrors.email}\n                  />\n\n                  <FieldError>{fieldErrors.email}</FieldError>\n                </Field>\n              )}\n\n              <div className=\"flex flex-col gap-3\">\n                <Button\n                  type=\"submit\"\n                  disabled={\n                    isPending ||\n                    isSigningIn ||\n                    (codeSent && code.length !== otpLength)\n                  }\n                >\n                  {(isSending || isSigningIn) && <Spinner />}\n\n                  {codeSent\n                    ? emailOtpLocalization.verifyCode\n                    : emailOtpLocalization.sendCode}\n                </Button>\n\n                {codeSent ? (\n                  <>\n                    <OpenEmailButton email={email} variant=\"secondary\" />\n\n                    <Button\n                      type=\"button\"\n                      variant=\"outline\"\n                      disabled={isPending || isSigningIn || isCoolingDown}\n                      onClick={sendCode}\n                    >\n                      {isCoolingDown\n                        ? localization.auth.resendIn.replace(\n                            \"{{seconds}}\",\n                            String(cooldown)\n                          )\n                        : localization.auth.resend}\n                    </Button>\n\n                    <Button\n                      type=\"button\"\n                      variant=\"ghost\"\n                      disabled={isPending || isSigningIn}\n                      onClick={() => {\n                        setCodeSent(false)\n                        setCode(\"\")\n                      }}\n                    >\n                      {emailOtpLocalization.useDifferentEmail}\n                    </Button>\n                  </>\n                ) : (\n                  plugins.flatMap((plugin) =>\n                    (plugin.authButtons ?? []).map((AuthButton, index) => (\n                      <AuthButton\n                        key={`${plugin.id}-${index.toString()}`}\n                        view=\"emailOtp\"\n                      />\n                    ))\n                  )\n                )}\n              </div>\n            </FieldGroup>\n          </form>\n\n          {socialPosition === \"bottom\" && !codeSent && (\n            <>\n              {showSeparator && (\n                <FieldSeparator className=\"*:data-[slot=field-separator-content]:bg-card text-xs flex items-center\">\n                  {localization.auth.or}\n                </FieldSeparator>\n              )}\n\n              {socialProviders && socialProviders.length > 0 && (\n                <ProviderButtons socialLayout={socialLayout} view=\"emailOtp\" />\n              )}\n            </>\n          )}\n        </div>\n\n        {emailAndPassword?.enabled && (\n          <div className=\"flex flex-col gap-3 items-center w-full mt-4\">\n            <FieldDescription className=\"text-center\">\n              {localization.auth.needToCreateAnAccount}{\" \"}\n              <Link\n                href={`${basePaths.auth}/${viewPaths.auth.signUp}`}\n                className=\"underline underline-offset-4\"\n              >\n                {localization.auth.signUp}\n              </Link>\n            </FieldDescription>\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/email-otp/email-otp.tsx"
    },
    {
      "path": "src/components/auth/email-otp/email-otp-button.tsx",
      "content": "\"use client\"\n\nimport {\n  type AuthView,\n  authMutationKeys,\n  getAuthLinkURL\n} from \"@better-auth-ui/core\"\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport { useIsMutating } from \"@tanstack/react-query\"\nimport { KeyRound, Lock } from \"lucide-react\"\n\nimport { buttonVariants } from \"@/components/ui/button\"\nimport { emailOtpPlugin } from \"@/lib/auth/email-otp-plugin\"\nimport { cn } from \"@/lib/utils\"\n\nexport type EmailOtpButtonProps = {\n  /** @remarks `AuthView` */\n  view?: AuthView\n}\n\n/**\n * Toggle button between password sign-in and the emailed-code route.\n *\n * @param view - Current auth view. On `\"emailOtp\"` this links back to password sign-in.\n */\nexport function EmailOtpButton({ view }: EmailOtpButtonProps) {\n  const {\n    basePaths,\n    emailAndPassword,\n    localization,\n    redirectTo,\n    viewPaths,\n    Link\n  } = useAuth()\n  const { localization: emailOtpLocalization, viewPaths: emailOtpViewPaths } =\n    useAuthPlugin(emailOtpPlugin)\n\n  const signInMutating = useIsMutating({\n    mutationKey: authMutationKeys.signIn.all\n  })\n  const signUpMutating = useIsMutating({\n    mutationKey: authMutationKeys.signUp.all\n  })\n  const isPending = signInMutating + signUpMutating > 0\n\n  const isEmailOtpView = view === \"emailOtp\"\n\n  // On the code view this button switches back to password sign-in. With\n  // password auth disabled there's nowhere to switch to, so hide it.\n  if (isEmailOtpView && !emailAndPassword?.enabled) return null\n\n  return (\n    <Link\n      href={getAuthLinkURL(\n        `${basePaths.auth}/${isEmailOtpView ? viewPaths.auth.signIn : emailOtpViewPaths.auth.emailOtp}`,\n        redirectTo\n      )}\n      aria-disabled={isPending || undefined}\n      tabIndex={isPending ? -1 : undefined}\n      onClick={(event) => {\n        if (isPending) event.preventDefault()\n      }}\n      className={cn(\n        buttonVariants({ variant: \"outline\" }),\n        \"w-full\",\n        isPending && \"opacity-50 pointer-events-none\"\n      )}\n    >\n      {isEmailOtpView ? <Lock /> : <KeyRound />}\n\n      {localization.auth.continueWith.replace(\n        \"{{provider}}\",\n        isEmailOtpView\n          ? localization.auth.password\n          : emailOtpLocalization.emailOtp\n      )}\n    </Link>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/email-otp/email-otp-button.tsx"
    },
    {
      "path": "src/components/auth/email-otp/verify-email-otp.tsx",
      "content": "\"use client\"\n\nimport { getAuthLinkURL } from \"@better-auth-ui/core\"\nimport {\n  type EmailOtpAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useSendVerificationOtp,\n  useVerifyEmailOtp\n} from \"@better-auth-ui/react\"\nimport { type SyntheticEvent, useEffect, useState } from \"react\"\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport {\n  Field,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { emailOtpPlugin } from \"@/lib/auth/email-otp-plugin\"\nimport {\n  RESEND_COOLDOWN_SECONDS,\n  useResendCooldown\n} from \"@/lib/auth/use-resend-cooldown\"\nimport { cn } from \"@/lib/utils\"\nimport { OpenEmailButton } from \"../open-email-button\"\nimport { OtpField } from \"../otp-field\"\nimport { useIsHydrated } from \"../use-is-hydrated\"\n\n/** `sessionStorage` key the sign-up and sign-in flows store the pending address under. */\nexport const VERIFY_EMAIL_STORAGE_KEY = \"better-auth-ui.verify-email\"\n\nexport type VerifyEmailOtpProps = {\n  className?: string\n}\n\n/**\n * Verify an email address with a code instead of a link.\n *\n * Replaces the built-in `<VerifyEmail />` view when the email-OTP plugin runs\n * with `emailVerification: true`. The address comes from session storage when\n * sign-up or sign-in put it there; otherwise the user types it and requests a\n * code. Sign-up already triggered a send, so the resend button starts on\n * cooldown just like the link-based view.\n *\n * @param className - Additional CSS classes applied to the card.\n */\nexport function VerifyEmailOtp({ className }: VerifyEmailOtpProps) {\n  const {\n    authClient,\n    basePaths,\n    localization,\n    navigate,\n    redirectTo,\n    viewPaths,\n    Link\n  } = useAuth()\n  const { localization: emailOtpLocalization, otpLength } =\n    useAuthPlugin(emailOtpPlugin)\n\n  const otpClient = authClient as EmailOtpAuthClient\n  const isHydrated = useIsHydrated()\n\n  const [email, setEmail] = useState(\n    (isHydrated && sessionStorage.getItem(VERIFY_EMAIL_STORAGE_KEY)) || \"\"\n  )\n  const [code, setCode] = useState(\"\")\n  const [fieldErrors, setFieldErrors] = useState<{ email?: string }>({})\n\n  const { cooldown, isCoolingDown, startCooldown } = useResendCooldown()\n\n  // Sign-up already sent a code to this address, so restoring it also starts\n  // the cooldown — otherwise the hydrated render would offer an immediate\n  // resend and walk straight into the server's rate limit.\n  useEffect(() => {\n    const pendingEmail = sessionStorage.getItem(VERIFY_EMAIL_STORAGE_KEY) ?? \"\"\n    setEmail(pendingEmail)\n\n    if (pendingEmail) startCooldown(RESEND_COOLDOWN_SECONDS)\n  }, [startCooldown])\n\n  const { mutate: sendVerificationOtp, isPending: isSending } =\n    useSendVerificationOtp(otpClient, {\n      onSuccess: (_data, { email: sentTo }) => {\n        sessionStorage.setItem(VERIFY_EMAIL_STORAGE_KEY, sentTo)\n        setEmail(sentTo)\n        startCooldown()\n        toast.success(emailOtpLocalization.codeSent)\n      }\n    })\n\n  const { mutate: verifyEmailOtp, isPending: isVerifying } = useVerifyEmailOtp(\n    otpClient,\n    {\n      onError: () => setCode(\"\"),\n      onSuccess: () => {\n        sessionStorage.removeItem(VERIFY_EMAIL_STORAGE_KEY)\n        toast.success(emailOtpLocalization.emailVerified)\n        navigate({ to: redirectTo })\n      }\n    }\n  )\n\n  const isPending = isSending || isVerifying\n\n  const verifyCode = (completedCode: string) => {\n    if (isPending || !email) return\n\n    verifyEmailOtp({ email, otp: completedCode })\n  }\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    if (!email) {\n      const formData = new FormData(e.currentTarget)\n      sendVerificationOtp({\n        email: formData.get(\"email\") as string,\n        type: \"email-verification\"\n      })\n      return\n    }\n\n    verifyCode(code)\n  }\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">\n          {localization.auth.verifyEmail}\n        </CardTitle>\n\n        {email && (\n          <CardDescription>\n            {emailOtpLocalization.codeSentTo.replace(\"{{email}}\", email)}\n          </CardDescription>\n        )}\n      </CardHeader>\n\n      <CardContent>\n        <form onSubmit={handleSubmit}>\n          <FieldGroup>\n            {email ? (\n              <OtpField\n                autoFocus\n                disabled={isPending}\n                label={emailOtpLocalization.code}\n                length={otpLength}\n                name=\"otp\"\n                value={code}\n                onChange={setCode}\n                onComplete={verifyCode}\n              />\n            ) : (\n              <Field data-invalid={!!fieldErrors.email}>\n                <FieldLabel htmlFor=\"email\">\n                  {localization.auth.email}\n                </FieldLabel>\n\n                <Input\n                  id=\"email\"\n                  name=\"email\"\n                  type=\"email\"\n                  autoComplete=\"email\"\n                  placeholder={localization.auth.emailPlaceholder}\n                  required\n                  disabled={isPending}\n                  onChange={() =>\n                    setFieldErrors((prev) => ({ ...prev, email: undefined }))\n                  }\n                  onInvalid={(e) => {\n                    e.preventDefault()\n\n                    setFieldErrors((prev) => ({\n                      ...prev,\n                      email: (e.target as HTMLInputElement).validationMessage\n                    }))\n                  }}\n                  aria-invalid={!!fieldErrors.email}\n                />\n\n                <FieldError>{fieldErrors.email}</FieldError>\n              </Field>\n            )}\n\n            <div className=\"flex flex-col gap-3\">\n              <Button\n                type=\"submit\"\n                disabled={\n                  isPending || (Boolean(email) && code.length !== otpLength)\n                }\n              >\n                {isPending && <Spinner />}\n\n                {email\n                  ? emailOtpLocalization.verifyCode\n                  : emailOtpLocalization.sendCode}\n              </Button>\n\n              {email && <OpenEmailButton email={email} variant=\"secondary\" />}\n\n              {email && (\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  disabled={isPending || isCoolingDown}\n                  onClick={() =>\n                    sendVerificationOtp({ email, type: \"email-verification\" })\n                  }\n                >\n                  {isCoolingDown\n                    ? localization.auth.resendIn.replace(\n                        \"{{seconds}}\",\n                        String(cooldown)\n                      )\n                    : localization.auth.resend}\n                </Button>\n              )}\n            </div>\n          </FieldGroup>\n        </form>\n\n        <div className=\"flex flex-col gap-3 items-center w-full mt-4\">\n          <FieldDescription className=\"text-center\">\n            {localization.auth.alreadyVerifiedYourEmail}{\" \"}\n            <Link\n              href={getAuthLinkURL(\n                `${basePaths.auth}/${viewPaths.auth.signIn}`,\n                redirectTo\n              )}\n              className=\"underline underline-offset-4\"\n            >\n              {localization.auth.signIn}\n            </Link>\n          </FieldDescription>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/email-otp/verify-email-otp.tsx"
    },
    {
      "path": "src/components/auth/email-otp/forgot-password-otp.tsx",
      "content": "\"use client\"\n\nimport { getAuthLinkURL } from \"@better-auth-ui/core\"\nimport {\n  type EmailOtpAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useFetchOptions,\n  useRequestPasswordResetOtp\n} from \"@better-auth-ui/react\"\nimport { type SyntheticEvent, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\"\nimport {\n  Field,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { emailOtpPlugin } from \"@/lib/auth/email-otp-plugin\"\nimport { cn } from \"@/lib/utils\"\n\n/** `sessionStorage` key the reset-code form reads the pending address from. */\nexport const RESET_PASSWORD_OTP_STORAGE_KEY =\n  \"better-auth-ui.reset-password-otp\"\n\nexport type ForgotPasswordOtpProps = {\n  className?: string\n}\n\n/**\n * Request a password-reset code instead of a reset link.\n *\n * Replaces the built-in `<ForgotPassword />` view when the email-OTP plugin\n * runs with `passwordReset: true`. On success the address is stored and the\n * user continues on `/auth/reset-password`, which asks for the code and the\n * new password — the reset-link-sent view is skipped entirely.\n *\n * @param className - Additional CSS classes applied to the card.\n */\nexport function ForgotPasswordOtp({ className }: ForgotPasswordOtpProps) {\n  const {\n    authClient,\n    basePaths,\n    localization,\n    navigate,\n    plugins,\n    redirectTo,\n    viewPaths,\n    Link\n  } = useAuth()\n  const { localization: emailOtpLocalization } = useAuthPlugin(emailOtpPlugin)\n\n  const { fetchOptions, resetFetchOptions } = useFetchOptions()\n  const [fieldErrors, setFieldErrors] = useState<{ email?: string }>({})\n\n  const { mutate: requestPasswordResetOtp, isPending } =\n    useRequestPasswordResetOtp(authClient as EmailOtpAuthClient, {\n      onError: () => resetFetchOptions(),\n      onSuccess: (_data, { email }) => {\n        sessionStorage.setItem(RESET_PASSWORD_OTP_STORAGE_KEY, email)\n        navigate({ to: `${basePaths.auth}/${viewPaths.auth.resetPassword}` })\n      }\n    })\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    const formData = new FormData(e.currentTarget)\n    requestPasswordResetOtp({\n      email: formData.get(\"email\") as string,\n      fetchOptions\n    })\n  }\n\n  const Captcha = plugins.find(\n    (plugin) => plugin.captchaComponent\n  )?.captchaComponent\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">\n          {localization.auth.forgotPassword}\n        </CardTitle>\n      </CardHeader>\n\n      <CardContent>\n        <form onSubmit={handleSubmit}>\n          <FieldGroup>\n            <Field data-invalid={!!fieldErrors.email}>\n              <FieldLabel htmlFor=\"email\">{localization.auth.email}</FieldLabel>\n\n              <Input\n                id=\"email\"\n                name=\"email\"\n                type=\"email\"\n                autoComplete=\"email\"\n                placeholder={localization.auth.emailPlaceholder}\n                required\n                disabled={isPending}\n                onChange={() =>\n                  setFieldErrors((prev) => ({ ...prev, email: undefined }))\n                }\n                onInvalid={(e) => {\n                  e.preventDefault()\n\n                  setFieldErrors((prev) => ({\n                    ...prev,\n                    email: (e.target as HTMLInputElement).validationMessage\n                  }))\n                }}\n                aria-invalid={!!fieldErrors.email}\n              />\n\n              <FieldError>{fieldErrors.email}</FieldError>\n            </Field>\n\n            {Captcha && <div className=\"flex justify-center\">{Captcha}</div>}\n\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending && <Spinner />}\n\n              {emailOtpLocalization.sendCode}\n            </Button>\n          </FieldGroup>\n        </form>\n\n        <div className=\"flex flex-col gap-3 items-center w-full mt-4\">\n          <FieldDescription className=\"text-center\">\n            {localization.auth.rememberYourPassword}{\" \"}\n            <Link\n              href={getAuthLinkURL(\n                `${basePaths.auth}/${viewPaths.auth.signIn}`,\n                redirectTo\n              )}\n              className=\"underline underline-offset-4\"\n            >\n              {localization.auth.signIn}\n            </Link>\n          </FieldDescription>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/email-otp/forgot-password-otp.tsx"
    },
    {
      "path": "src/components/auth/email-otp/reset-password-otp.tsx",
      "content": "\"use client\"\n\nimport { getAuthLinkURL } from \"@better-auth-ui/core\"\nimport {\n  type EmailOtpAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useResetPasswordOtp\n} from \"@better-auth-ui/react\"\nimport { Eye, EyeOff } from \"lucide-react\"\nimport { type SyntheticEvent, useEffect, useRef, useState } from \"react\"\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport {\n  Field,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput\n} from \"@/components/ui/input-group\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { emailOtpPlugin } from \"@/lib/auth/email-otp-plugin\"\nimport { cn } from \"@/lib/utils\"\nimport { OpenEmailButton } from \"../open-email-button\"\nimport { OtpField } from \"../otp-field\"\nimport { useIsHydrated } from \"../use-is-hydrated\"\nimport { RESET_PASSWORD_OTP_STORAGE_KEY } from \"./forgot-password-otp\"\n\nexport type ResetPasswordOtpProps = {\n  className?: string\n}\n\n/**\n * Reset a password with an emailed code.\n *\n * Replaces the built-in `<ResetPassword />` view when the email-OTP plugin\n * runs with `passwordReset: true`. There is no token in the URL — the code\n * and the new password are submitted together. The address comes from the\n * forgot-password step, and is asked for again when it isn't there (e.g. the\n * user finishes on another tab).\n *\n * @param className - Additional CSS classes applied to the card.\n */\nexport function ResetPasswordOtp({ className }: ResetPasswordOtpProps) {\n  const {\n    authClient,\n    basePaths,\n    emailAndPassword,\n    localization,\n    navigate,\n    redirectTo,\n    viewPaths,\n    Link\n  } = useAuth()\n  const { localization: emailOtpLocalization, otpLength } =\n    useAuthPlugin(emailOtpPlugin)\n\n  const isHydrated = useIsHydrated()\n  const initialEmail =\n    (isHydrated && sessionStorage.getItem(RESET_PASSWORD_OTP_STORAGE_KEY)) || \"\"\n  const [email, setEmail] = useState(initialEmail)\n  const [hasStoredEmail, setHasStoredEmail] = useState(Boolean(initialEmail))\n  const [code, setCode] = useState(\"\")\n  const [isPasswordVisible, setIsPasswordVisible] = useState(false)\n  const formRef = useRef<HTMLFormElement>(null)\n  const submissionLockedRef = useRef(false)\n  const [fieldErrors, setFieldErrors] = useState<{\n    email?: string\n    password?: string\n  }>({})\n\n  useEffect(() => {\n    const storedEmail =\n      sessionStorage.getItem(RESET_PASSWORD_OTP_STORAGE_KEY) ?? \"\"\n    setEmail(storedEmail)\n    setHasStoredEmail(Boolean(storedEmail))\n  }, [])\n\n  const { mutate: resetPasswordOtp, isPending } = useResetPasswordOtp(\n    authClient as EmailOtpAuthClient,\n    {\n      onError: () => {\n        submissionLockedRef.current = false\n        setCode(\"\")\n      },\n      onSuccess: () => {\n        sessionStorage.removeItem(RESET_PASSWORD_OTP_STORAGE_KEY)\n        toast.success(localization.auth.passwordResetSuccess)\n        navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })\n      }\n    }\n  )\n\n  const submitReset = (\n    form: HTMLFormElement,\n    submittedCode: string,\n    reportErrors: boolean\n  ) => {\n    if (isPending || submissionLockedRef.current) return\n\n    const formData = new FormData(form)\n    const password = formData.get(\"password\") as string\n    const confirmPassword = formData.get(\"confirmPassword\") as string\n    const submittedEmail = hasStoredEmail\n      ? email\n      : (formData.get(\"email\") as string)\n\n    if (emailAndPassword?.confirmPassword && password !== confirmPassword) {\n      if (reportErrors) {\n        toast.error(localization.auth.passwordsDoNotMatch)\n      }\n      return\n    }\n\n    if (submittedCode.length !== otpLength) {\n      if (reportErrors) {\n        toast.error(\n          emailOtpLocalization.codeLengthMismatch.replace(\n            \"{{length}}\",\n            String(otpLength)\n          )\n        )\n      }\n      return\n    }\n\n    submissionLockedRef.current = true\n    resetPasswordOtp({ email: submittedEmail, otp: submittedCode, password })\n  }\n\n  const tryAutoSubmit = (completedCode?: string) => {\n    const form = formRef.current\n\n    if (!form?.matches(\":valid\")) return\n\n    const formData = new FormData(form)\n    const submittedCode = completedCode ?? String(formData.get(\"otp\") ?? \"\")\n\n    submitReset(form, submittedCode, false)\n  }\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n    submitReset(e.currentTarget, code, true)\n  }\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-xl font-semibold\">\n          {localization.auth.resetPassword}\n        </CardTitle>\n\n        {hasStoredEmail && email && (\n          <CardDescription>\n            {emailOtpLocalization.codeSentTo.replace(\"{{email}}\", email)}\n          </CardDescription>\n        )}\n      </CardHeader>\n\n      <CardContent>\n        <form ref={formRef} onSubmit={handleSubmit}>\n          <FieldGroup>\n            {!hasStoredEmail && (\n              <Field data-invalid={!!fieldErrors.email}>\n                <FieldLabel htmlFor=\"email\">\n                  {localization.auth.email}\n                </FieldLabel>\n\n                <Input\n                  id=\"email\"\n                  name=\"email\"\n                  type=\"email\"\n                  autoComplete=\"email\"\n                  value={email}\n                  placeholder={localization.auth.emailPlaceholder}\n                  required\n                  disabled={isPending}\n                  onChange={(event) => {\n                    setEmail(event.target.value)\n                    setFieldErrors((prev) => ({ ...prev, email: undefined }))\n                  }}\n                  onInvalid={(e) => {\n                    e.preventDefault()\n\n                    setFieldErrors((prev) => ({\n                      ...prev,\n                      email: (e.target as HTMLInputElement).validationMessage\n                    }))\n                  }}\n                  aria-invalid={!!fieldErrors.email}\n                />\n\n                <FieldError>{fieldErrors.email}</FieldError>\n              </Field>\n            )}\n\n            <OtpField\n              autoFocus={hasStoredEmail}\n              disabled={isPending}\n              label={emailOtpLocalization.code}\n              length={otpLength}\n              name=\"otp\"\n              value={code}\n              onChange={setCode}\n              onComplete={tryAutoSubmit}\n            />\n\n            <Field data-invalid={!!fieldErrors.password}>\n              <FieldLabel htmlFor=\"password\">\n                {localization.auth.newPassword}\n              </FieldLabel>\n\n              <InputGroup>\n                <InputGroupInput\n                  id=\"password\"\n                  name=\"password\"\n                  type={isPasswordVisible ? \"text\" : \"password\"}\n                  autoComplete=\"new-password\"\n                  placeholder={localization.auth.newPasswordPlaceholder}\n                  required\n                  minLength={emailAndPassword?.minPasswordLength}\n                  maxLength={emailAndPassword?.maxPasswordLength}\n                  disabled={isPending}\n                  onChange={() =>\n                    setFieldErrors((prev) => ({ ...prev, password: undefined }))\n                  }\n                  onInvalid={(e) => {\n                    e.preventDefault()\n                    const el = e.target as HTMLInputElement\n                    const min = emailAndPassword?.minPasswordLength\n                    const max = emailAndPassword?.maxPasswordLength\n                    const msg = el.validity.valueMissing\n                      ? localization.auth.fieldRequired\n                      : el.validity.tooShort\n                        ? localization.auth.tooShort.replace(\n                            \"{{min}}\",\n                            String(min)\n                          )\n                        : localization.auth.tooLong.replace(\n                            \"{{max}}\",\n                            String(max)\n                          )\n\n                    setFieldErrors((prev) => ({ ...prev, password: msg }))\n                  }}\n                  aria-invalid={!!fieldErrors.password}\n                />\n\n                <InputGroupAddon align=\"inline-end\">\n                  <InputGroupButton\n                    size=\"icon-xs\"\n                    aria-label={\n                      isPasswordVisible\n                        ? localization.auth.hidePassword\n                        : localization.auth.showPassword\n                    }\n                    title={\n                      isPasswordVisible\n                        ? localization.auth.hidePassword\n                        : localization.auth.showPassword\n                    }\n                    onClick={() => setIsPasswordVisible((visible) => !visible)}\n                  >\n                    {isPasswordVisible ? <EyeOff /> : <Eye />}\n                  </InputGroupButton>\n                </InputGroupAddon>\n              </InputGroup>\n\n              <FieldError>{fieldErrors.password}</FieldError>\n            </Field>\n\n            {emailAndPassword?.confirmPassword && (\n              <Field>\n                <FieldLabel htmlFor=\"confirmPassword\">\n                  {localization.auth.confirmPassword}\n                </FieldLabel>\n\n                <Input\n                  id=\"confirmPassword\"\n                  name=\"confirmPassword\"\n                  type=\"password\"\n                  autoComplete=\"new-password\"\n                  placeholder={localization.auth.confirmPasswordPlaceholder}\n                  required\n                  minLength={emailAndPassword?.minPasswordLength}\n                  maxLength={emailAndPassword?.maxPasswordLength}\n                  disabled={isPending}\n                />\n              </Field>\n            )}\n\n            <div className=\"flex flex-col gap-3\">\n              <Button type=\"submit\" disabled={isPending}>\n                {isPending && <Spinner />}\n\n                {localization.auth.resetPassword}\n              </Button>\n\n              {email && <OpenEmailButton email={email} variant=\"secondary\" />}\n            </div>\n          </FieldGroup>\n        </form>\n\n        <div className=\"flex flex-col gap-3 items-center w-full mt-4\">\n          <FieldDescription className=\"text-center\">\n            {localization.auth.rememberYourPassword}{\" \"}\n            <Link\n              href={getAuthLinkURL(\n                `${basePaths.auth}/${viewPaths.auth.signIn}`,\n                redirectTo\n              )}\n              className=\"underline underline-offset-4\"\n            >\n              {localization.auth.signIn}\n            </Link>\n          </FieldDescription>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/email-otp/reset-password-otp.tsx"
    },
    {
      "path": "src/components/auth/email-otp/change-email-otp.tsx",
      "content": "\"use client\"\n\nimport {\n  type EmailOtpAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useChangeEmailOtp,\n  useRequestEmailChangeOtp,\n  useSendVerificationOtp,\n  useSession\n} from \"@better-auth-ui/react\"\nimport { type SyntheticEvent, useReducer, useState } from \"react\"\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent, CardFooter } from \"@/components/ui/card\"\nimport { Field, FieldError, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { emailOtpPlugin } from \"@/lib/auth/email-otp-plugin\"\nimport { cn } from \"@/lib/utils\"\nimport { OpenEmailButton } from \"../open-email-button\"\nimport { OtpField } from \"../otp-field\"\n\ntype ChangeEmailStep = \"email\" | \"currentCode\" | \"newCode\"\n\ntype ChangeEmailState = {\n  step: ChangeEmailStep\n  newEmail: string\n}\n\ntype ChangeEmailAction =\n  | { type: \"currentEmailChallenged\"; newEmail: string }\n  | { type: \"changeRequested\"; newEmail: string }\n  | { type: \"restarted\" }\n\nconst initialChangeEmailState: ChangeEmailState = {\n  step: \"email\",\n  newEmail: \"\"\n}\n\n// Every step is reachable from the action alone, so the previous state never\n// takes part in the transition.\nfunction changeEmailReducer(\n  _state: ChangeEmailState,\n  action: ChangeEmailAction\n): ChangeEmailState {\n  switch (action.type) {\n    case \"currentEmailChallenged\":\n      return { step: \"currentCode\", newEmail: action.newEmail }\n    case \"changeRequested\":\n      return { step: \"newCode\", newEmail: action.newEmail }\n    case \"restarted\":\n      return initialChangeEmailState\n  }\n}\n\nexport type ChangeEmailOtpProps = {\n  className?: string\n}\n\n/**\n * Change the account email with codes instead of a confirmation link.\n *\n * Replaces the built-in `<ChangeEmail />` card when the email-OTP plugin runs\n * with `changeEmail: true`. With `verifyCurrentEmail` on it is a three-step\n * flow — confirm the current address, then the new one — and two steps\n * otherwise.\n *\n * @param className - Additional CSS classes applied to the card.\n */\nexport function ChangeEmailOtp({ className }: ChangeEmailOtpProps) {\n  const { authClient, localization } = useAuth()\n  const {\n    localization: emailOtpLocalization,\n    otpLength,\n    verifyCurrentEmail\n  } = useAuthPlugin(emailOtpPlugin)\n\n  const otpClient = authClient as EmailOtpAuthClient\n  const { data: session } = useSession(otpClient)\n  const currentEmail = session?.user.email\n\n  const [state, dispatch] = useReducer(\n    changeEmailReducer,\n    initialChangeEmailState\n  )\n  const [code, setCode] = useState(\"\")\n  const [fieldErrors, setFieldErrors] = useState<{ email?: string }>({})\n\n  const resetFlow = () => {\n    setCode(\"\")\n    dispatch({ type: \"restarted\" })\n  }\n\n  // The step transition is attached per call: the code goes to the current\n  // address while the pending change targets the new one, so the address to\n  // remember isn't in this mutation's variables.\n  const { mutate: sendVerificationOtp, isPending: isSending } =\n    useSendVerificationOtp(otpClient)\n\n  const { mutate: requestEmailChangeOtp, isPending: isRequesting } =\n    useRequestEmailChangeOtp(otpClient, {\n      onError: () => setCode(\"\"),\n      onSuccess: (_data, { newEmail }) => {\n        setCode(\"\")\n        dispatch({ type: \"changeRequested\", newEmail })\n      }\n    })\n\n  const { mutate: changeEmailOtp, isPending: isChanging } = useChangeEmailOtp(\n    otpClient,\n    {\n      onError: () => setCode(\"\"),\n      onSuccess: () => {\n        toast.success(localization.settings.changeEmailSuccess)\n        resetFlow()\n      }\n    }\n  )\n\n  const isPending = isSending || isRequesting || isChanging\n\n  const submitCode = (completedCode: string) => {\n    if (isPending || state.step === \"email\") return\n\n    if (state.step === \"currentCode\") {\n      requestEmailChangeOtp({\n        newEmail: state.newEmail,\n        otp: completedCode\n      })\n      return\n    }\n\n    changeEmailOtp({ newEmail: state.newEmail, otp: completedCode })\n  }\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    if (state.step === \"email\") {\n      const formData = new FormData(e.currentTarget)\n      const newEmail = formData.get(\"email\") as string\n\n      if (verifyCurrentEmail && currentEmail) {\n        sendVerificationOtp(\n          { email: currentEmail, type: \"change-email\" },\n          {\n            onSuccess: () =>\n              dispatch({ type: \"currentEmailChallenged\", newEmail })\n          }\n        )\n        return\n      }\n\n      requestEmailChangeOtp({ newEmail })\n      return\n    }\n\n    submitCode(code)\n  }\n\n  const codeTarget =\n    state.step === \"currentCode\" ? currentEmail : state.newEmail\n\n  return (\n    <div>\n      <h2 className=\"text-sm font-semibold mb-3\">\n        {localization.settings.changeEmail}\n      </h2>\n\n      <form onSubmit={handleSubmit}>\n        <Card className={cn(className)}>\n          <CardContent className=\"flex flex-col gap-6\">\n            {state.step === \"email\" ? (\n              <Field data-invalid={!!fieldErrors.email}>\n                <FieldLabel htmlFor=\"email\">\n                  {localization.auth.email}\n                </FieldLabel>\n\n                {session ? (\n                  <Input\n                    key={currentEmail}\n                    id=\"email\"\n                    name=\"email\"\n                    type=\"email\"\n                    autoComplete=\"email\"\n                    defaultValue={currentEmail}\n                    placeholder={localization.auth.emailPlaceholder}\n                    disabled={isPending}\n                    required\n                    onChange={() =>\n                      setFieldErrors((prev) => ({ ...prev, email: undefined }))\n                    }\n                    onInvalid={(e) => {\n                      e.preventDefault()\n\n                      setFieldErrors((prev) => ({\n                        ...prev,\n                        email: (e.target as HTMLInputElement).validationMessage\n                      }))\n                    }}\n                    aria-invalid={!!fieldErrors.email}\n                  />\n                ) : (\n                  <Skeleton>\n                    <Input className=\"invisible\" />\n                  </Skeleton>\n                )}\n\n                <FieldError>{fieldErrors.email}</FieldError>\n              </Field>\n            ) : (\n              <div className=\"flex flex-col gap-3\">\n                <p className=\"text-muted-foreground text-sm\">\n                  {emailOtpLocalization.confirmEmailDescription.replace(\n                    \"{{email}}\",\n                    codeTarget ?? \"\"\n                  )}\n                </p>\n\n                <OtpField\n                  autoFocus\n                  disabled={isPending}\n                  label={\n                    state.step === \"currentCode\"\n                      ? emailOtpLocalization.confirmCurrentEmail\n                      : emailOtpLocalization.confirmNewEmail\n                  }\n                  length={otpLength}\n                  name=\"otp\"\n                  value={code}\n                  onChange={setCode}\n                  onComplete={submitCode}\n                />\n\n                {codeTarget && (\n                  <OpenEmailButton email={codeTarget} variant=\"secondary\" />\n                )}\n              </div>\n            )}\n          </CardContent>\n\n          <CardFooter className=\"gap-3\">\n            {state.step !== \"email\" && (\n              <Button\n                type=\"button\"\n                size=\"sm\"\n                variant=\"outline\"\n                disabled={isPending}\n                onClick={resetFlow}\n              >\n                {localization.settings.cancel}\n              </Button>\n            )}\n\n            <Button\n              type=\"submit\"\n              size=\"sm\"\n              disabled={\n                isPending ||\n                !session ||\n                (state.step !== \"email\" && code.length !== otpLength)\n              }\n            >\n              {isPending && <Spinner />}\n\n              {state.step === \"email\"\n                ? localization.settings.updateEmail\n                : emailOtpLocalization.verifyCode}\n            </Button>\n          </CardFooter>\n        </Card>\n      </form>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/email-otp/change-email-otp.tsx"
    },
    {
      "path": "src/components/auth/open-email-button.tsx",
      "content": "\"use client\"\n\nimport { createQrCodeSvgData, getEmailProviderLink } from \"@better-auth-ui/core\"\nimport { useAuth } from \"@better-auth-ui/react\"\nimport type { VariantProps } from \"class-variance-authority\"\nimport { QrCode } from \"lucide-react\"\nimport { useMemo } from \"react\"\n\nimport { buttonVariants } from \"@/components/ui/button\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger\n} from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\n\nexport type OpenEmailButtonProps = {\n  /** Email address used to detect the provider, e.g. from the verify-email flow. */\n  email: string\n  className?: string\n  /**\n   * Button variant. Defaults to the primary style for dead-end views where\n   * opening the inbox is the only action; pass `\"secondary\"` where it sits\n   * beside a submit button that should stay the primary call to action.\n   */\n  variant?: VariantProps<typeof buttonVariants>[\"variant\"]\n}\n\n/**\n * Render a button that opens the user's email provider login page in a new\n * tab. Hovering or focusing the button reveals a QR code for opening the same\n * provider on another device.\n *\n * The provider is resolved from the email domain via the curated\n * `@mikkelscheike/email-provider-links` dataset (Gmail, Outlook, GMX, etc.).\n * Renders nothing when the domain is empty or not a known provider.\n *\n * @param email - Email address to resolve the provider from.\n * @param className - Additional CSS classes applied to the button.\n * @param variant - Button variant. Defaults to the primary style.\n * @returns The open-email button, or `null` when no provider matches.\n */\nexport function OpenEmailButton({\n  email,\n  className,\n  variant\n}: OpenEmailButtonProps) {\n  const { localization } = useAuth()\n\n  const provider = getEmailProviderLink(email)\n  const loginUrl = provider?.loginUrl\n  const qrCode = useMemo(\n    () => (loginUrl ? createQrCodeSvgData(loginUrl) : null),\n    [loginUrl]\n  )\n\n  if (!provider || !qrCode) return null\n\n  const scanLabel = localization.auth.scanToOpenEmailProvider.replace(\n    \"{{provider}}\",\n    provider.companyProvider\n  )\n\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger\n          type=\"button\"\n          className={cn(buttonVariants({ variant }), \"w-full\", className)}\n          onClick={() =>\n            window.open(provider.loginUrl, \"_blank\", \"noopener,noreferrer\")\n          }\n        >\n          {localization.auth.openEmailProvider.replace(\n            \"{{provider}}\",\n            provider.companyProvider\n          )}\n          <QrCode data-icon=\"inline-end\" />\n        </TooltipTrigger>\n        <TooltipContent\n          sideOffset={8}\n          className=\"flex-col items-center gap-2 p-3\"\n        >\n          <svg\n            viewBox={`0 0 ${qrCode.size} ${qrCode.size}`}\n            aria-hidden=\"true\"\n            focusable=\"false\"\n            className=\"size-40\"\n          >\n            <path fill=\"white\" d={`M0 0h${qrCode.size}v${qrCode.size}H0z`} />\n            <path fill=\"black\" d={qrCode.path} shapeRendering=\"crispEdges\" />\n          </svg>\n          <p className=\"max-w-40 text-center leading-snug\">{scanLabel}</p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/open-email-button.tsx"
    },
    {
      "path": "src/components/auth/provider-button.tsx",
      "content": "\"use client\"\n\nimport {\n  type AuthView,\n  authMutationKeys,\n  getProviderName\n} from \"@better-auth-ui/core\"\nimport { providerIcons, useAuth, useSignInSocial } from \"@better-auth-ui/react\"\nimport { useIsMutating } from \"@tanstack/react-query\"\nimport type { SocialProvider } from \"better-auth/social-providers\"\nimport type { ComponentProps } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { cn } from \"@/lib/utils\"\nimport { LastUsedBadge } from \"./last-login-method/last-used-badge\"\n\nexport type ProviderButtonProps = {\n  provider: SocialProvider\n  display?: \"full\" | \"name\" | \"icon\"\n  view?: AuthView\n} & Omit<ComponentProps<typeof Button>, \"onClick\" | \"children\" | \"disabled\">\n\n/**\n * Social provider sign-in button.\n *\n * @param provider - Provider to sign in with.\n * @param display - `\"full\"` (e.g. \"Continue with Google\"), `\"name\"` (just the provider name), or `\"icon\"` (icon only).\n */\nexport function ProviderButton({\n  provider,\n  display = \"full\",\n  view = \"signIn\",\n  variant = \"outline\",\n  className,\n  ...props\n}: ProviderButtonProps) {\n  const { authClient, baseURL, localization, redirectTo } = useAuth()\n\n  const callbackURL = `${baseURL}${redirectTo}`\n\n  const { mutate: signInSocial, isPending: signInSocialPending } =\n    useSignInSocial(authClient)\n\n  const ProviderIcon = providerIcons[provider]\n\n  const signInMutating = useIsMutating({\n    mutationKey: authMutationKeys.signIn.all\n  })\n  const signUpMutating = useIsMutating({\n    mutationKey: authMutationKeys.signUp.all\n  })\n  const isPending = signInMutating + signUpMutating > 0\n\n  return (\n    <Button\n      type=\"button\"\n      variant={variant}\n      disabled={isPending}\n      onClick={() => signInSocial({ provider, callbackURL })}\n      className={cn(\"relative overflow-visible\", className)}\n      {...props}\n    >\n      {signInSocialPending ? (\n        <Spinner />\n      ) : ProviderIcon ? (\n        <ProviderIcon />\n      ) : null}\n\n      {display === \"full\"\n        ? localization.auth.continueWith.replace(\n            \"{{provider}}\",\n            getProviderName(provider)\n          )\n        : display === \"name\"\n          ? getProviderName(provider)\n          : null}\n\n      {display === \"icon\" && (\n        <span className=\"sr-only\">{getProviderName(provider)}</span>\n      )}\n\n      {view !== \"signUp\" && <LastUsedBadge method={provider} floating />}\n    </Button>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/provider-button.tsx"
    },
    {
      "path": "src/components/auth/provider-buttons.tsx",
      "content": "\"use client\"\n\nimport type { AuthView } from \"@better-auth-ui/core\"\nimport { useAuth } from \"@better-auth-ui/react\"\nimport { useMemo } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ProviderButton } from \"./provider-button\"\n\nexport type ProviderButtonsProps = {\n  socialLayout?: SocialLayout\n  view?: AuthView\n}\n\nexport type SocialLayout = \"auto\" | \"horizontal\" | \"vertical\" | \"grid\"\n\n/**\n * Render sign-in buttons for configured social providers. Each button owns its own sign-in mutation\n * and reads the shared sign-in pending state from React Query.\n *\n * @param socialLayout - Preferred layout for the provider buttons; `\"auto\"` chooses based on the number of providers.\n */\nexport function ProviderButtons({\n  socialLayout = \"auto\",\n  view = \"signIn\"\n}: ProviderButtonsProps) {\n  const { socialProviders } = useAuth()\n\n  const resolvedSocialLayout = useMemo(() => {\n    if (socialLayout === \"auto\") {\n      if (socialProviders?.length && socialProviders.length >= 4) {\n        return \"horizontal\"\n      }\n\n      return \"vertical\"\n    }\n\n    return socialLayout\n  }, [socialLayout, socialProviders?.length])\n\n  return (\n    <div\n      className={cn(\n        \"gap-3\",\n        resolvedSocialLayout === \"grid\" && \"grid grid-cols-2\",\n        resolvedSocialLayout === \"vertical\" && \"flex flex-col\",\n        resolvedSocialLayout === \"horizontal\" && \"flex flex-row flex-wrap\"\n      )}\n    >\n      {socialProviders?.map((provider) => (\n        <ProviderButton\n          key={provider}\n          provider={provider}\n          view={view}\n          display={\n            resolvedSocialLayout === \"vertical\"\n              ? \"full\"\n              : resolvedSocialLayout === \"grid\"\n                ? \"name\"\n                : \"icon\"\n          }\n          className={cn(resolvedSocialLayout === \"horizontal\" && \"flex-1\")}\n        />\n      ))}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/provider-buttons.tsx"
    },
    {
      "path": "src/components/auth/last-login-method/last-used-badge.tsx",
      "content": "\"use client\"\n\nimport { useLastLoginMethod } from \"@better-auth-ui/react\"\n\nimport { Badge } from \"@/components/ui/badge\"\n\nexport type LastUsedBadgeProps = {\n  /** Login method IDs that should display the indicator. */\n  method: string | string[]\n  /** Use the shorter label in constrained layouts. */\n  compact?: boolean\n  /** Float the compact indicator over the top-right edge of its container. */\n  floating?: boolean\n}\n\n/**\n * Displays an indicator when one of the supplied method IDs matches Better\n * Auth's stored last login method.\n */\nexport function LastUsedBadge({\n  method,\n  compact,\n  floating\n}: LastUsedBadgeProps) {\n  const { method: lastLoginMethod, localization } = useLastLoginMethod()\n  const methods = Array.isArray(method) ? method : [method]\n\n  if (!lastLoginMethod || !methods.includes(lastLoginMethod)) return null\n\n  return (\n    <Badge\n      className={\n        floating\n          ? \"pointer-events-none absolute top-0 right-0 z-10 translate-x-1/4 -translate-y-1/2 shadow-sm\"\n          : undefined\n      }\n      variant=\"secondary\"\n    >\n      {compact || floating ? localization.lastUsedShort : localization.lastUsed}\n    </Badge>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/last-login-method/last-used-badge.tsx"
    },
    {
      "path": "src/components/auth/use-is-hydrated.ts",
      "content": "\"use client\"\n\nimport { useSyncExternalStore } from \"react\"\n\n/**\n * Returns `true` once the component is mounted on the client (hydrated) and\n * `false` while rendering on the server, so client-only reads (e.g.\n * `sessionStorage`) stay safe during SSR.\n *\n * @returns Whether the component has hydrated on the client.\n */\nexport function useIsHydrated() {\n  const subscribe = () => () => {}\n  return useSyncExternalStore(\n    subscribe,\n    () => true,\n    () => false\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/use-is-hydrated.ts"
    }
  ],
  "type": "registry:component"
}