{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "two-factor",
  "title": "Two-Factor",
  "description": "Two-factor plugin: the post-password challenge for authenticator, emailed, and backup codes, plus the settings card for enrollment and backup-code management.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "better-auth",
    "input-otp",
    "lucide-react"
  ],
  "registryDependencies": [
    "alert-dialog",
    "button",
    "card",
    "checkbox",
    "dialog",
    "field",
    "input",
    "input-group",
    "input-otp",
    "skeleton",
    "sonner",
    "spinner",
    "https://better-auth-ui.com/r/radix-nova/sign-in.json",
    "https://better-auth-ui.com/r/radix-nova/username.json"
  ],
  "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/two-factor-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  twoFactorPlugin as coreTwoFactorPlugin,\n  type TwoFactorPluginOptions\n} from \"@better-auth-ui/core/plugins\"\n\nimport { TwoFactorChallenge } from \"@/components/auth/two-factor/two-factor-challenge\"\nimport { TwoFactorSettings } from \"@/components/auth/two-factor/two-factor-settings\"\n\nexport const twoFactorPlugin = createAuthPlugin(\n  coreTwoFactorPlugin.id,\n  (options: TwoFactorPluginOptions = {}) => ({\n    ...coreTwoFactorPlugin(options),\n    securityCards: [TwoFactorSettings],\n    views: {\n      auth: { twoFactor: TwoFactorChallenge }\n    }\n  })\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/two-factor-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/lib/auth/use-two-factor-password.ts",
      "content": "\"use client\"\n\nimport { useAuth, useAuthPlugin, useListAccounts } from \"@better-auth-ui/react\"\n\nimport { twoFactorPlugin } from \"./two-factor-plugin\"\n\n/**\n * Whether two-factor management should ask for the account password.\n *\n * Better Auth's `allowPasswordless` option only waives the password for\n * accounts that have no credential account — a passkey-only user has no\n * password to type. Reading the linked accounts keeps the UI in step with\n * that rule instead of guessing from the option alone.\n */\nexport function useTwoFactorPasswordRequirement() {\n  const { authClient } = useAuth()\n  const { allowPasswordless } = useAuthPlugin(twoFactorPlugin)\n  const { data: accounts, isPending } = useListAccounts(authClient)\n\n  const hasCredentialAccount = accounts?.some(\n    (account) => account.providerId === \"credential\"\n  )\n\n  return {\n    isPending: allowPasswordless && isPending,\n    // Default to asking while the account list loads. Guessing \"no password\"\n    // early would submit an empty body for a credential user and fail the\n    // request; guessing \"password\" costs a passkey-only user nothing, because\n    // the field disappears as soon as the list resolves.\n    requiresPassword:\n      !allowPasswordless || isPending || Boolean(hasCredentialAccount)\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/auth/use-two-factor-password.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/two-factor/two-factor-challenge.tsx",
      "content": "\"use client\"\n\nimport {\n  type TwoFactorAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useSendTwoFactorOtp,\n  useVerifyBackupCode,\n  useVerifyTotp,\n  useVerifyTwoFactorOtp\n} from \"@better-auth-ui/react\"\nimport { type SyntheticEvent, useEffect, 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 { Checkbox } from \"@/components/ui/checkbox\"\nimport {\n  Field,\n  FieldDescription,\n  FieldGroup,\n  FieldLabel\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport {\n  clearTwoFactorMethods,\n  readTwoFactorMethods,\n  type TwoFactorMethod\n} from \"@/lib/auth/two-factor-methods\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\nimport {\n  RESEND_COOLDOWN_SECONDS,\n  useResendCooldown\n} from \"@/lib/auth/use-resend-cooldown\"\nimport { cn } from \"@/lib/utils\"\nimport { OtpField } from \"../otp-field\"\nimport { useIsHydrated } from \"../use-is-hydrated\"\n\n/** Challenge surfaces the view can render, in the order they are offered. */\ntype ChallengeMethod = TwoFactorMethod | \"backup\"\n\nexport type TwoFactorChallengeProps = {\n  className?: string\n}\n\n/**\n * Second-factor challenge that finishes a pending sign-in.\n *\n * Better Auth answers a password sign-in with `twoFactorRedirect` instead of\n * a session, and the shared sign-in continuation sends the browser here with\n * the enabled methods in session storage. Verifying is what creates the\n * session, after which the original `redirectTo` is resumed.\n *\n * @param className - Additional CSS classes applied to the card.\n */\nexport function TwoFactorChallenge({ className }: TwoFactorChallengeProps) {\n  const {\n    authClient,\n    basePaths,\n    localization,\n    navigate,\n    redirectTo,\n    viewPaths,\n    Link\n  } = useAuth()\n  const {\n    backupCodes: backupCodesEnabled,\n    codeLength,\n    localization: twoFactorLocalization,\n    trustDevice: trustDeviceEnabled\n  } = useAuthPlugin(twoFactorPlugin)\n\n  const twoFactorClient = authClient as TwoFactorAuthClient\n  const isHydrated = useIsHydrated()\n\n  const [methods, setMethods] = useState<TwoFactorMethod[]>(() =>\n    isHydrated ? readTwoFactorMethods() : [\"totp\", \"otp\"]\n  )\n  const [method, setMethod] = useState<ChallengeMethod>(\n    () => methods[0] ?? \"totp\"\n  )\n  const [code, setCode] = useState(\"\")\n  const [trustDevice, setTrustDevice] = useState(false)\n  const [otpRequested, setOtpRequested] = useState(false)\n  const { cooldown, isCoolingDown, startCooldown } = useResendCooldown()\n\n  useEffect(() => {\n    const stored = readTwoFactorMethods()\n    setMethods(stored)\n    setMethod(stored[0] ?? \"totp\")\n  }, [])\n\n  const onVerified = () => {\n    clearTwoFactorMethods()\n    navigate({ to: redirectTo })\n  }\n\n  const { mutate: sendTwoFactorOtp, isPending: isSendingOtp } =\n    useSendTwoFactorOtp(twoFactorClient, {\n      onSuccess: () => {\n        setOtpRequested(true)\n        startCooldown(RESEND_COOLDOWN_SECONDS)\n      }\n    })\n\n  const { mutate: verifyTotp, isPending: isVerifyingTotp } = useVerifyTotp(\n    twoFactorClient,\n    { onError: () => setCode(\"\"), onSuccess: onVerified }\n  )\n\n  const { mutate: verifyTwoFactorOtp, isPending: isVerifyingOtp } =\n    useVerifyTwoFactorOtp(twoFactorClient, {\n      onError: () => setCode(\"\"),\n      onSuccess: onVerified\n    })\n\n  const { mutate: verifyBackupCode, isPending: isVerifyingBackupCode } =\n    useVerifyBackupCode(twoFactorClient, { onSuccess: onVerified })\n\n  const isPending =\n    isSendingOtp || isVerifyingTotp || isVerifyingOtp || isVerifyingBackupCode\n  const needsOtpRequest = method === \"otp\" && !otpRequested\n\n  const switchMethod = (next: ChallengeMethod) => {\n    setCode(\"\")\n    setMethod(next)\n  }\n\n  const verifyCode = (completedCode: string) => {\n    if (\n      isPending ||\n      needsOtpRequest ||\n      method === \"backup\" ||\n      completedCode.length !== codeLength\n    ) {\n      return\n    }\n\n    const trust = trustDeviceEnabled ? { trustDevice } : {}\n\n    if (method === \"otp\") {\n      verifyTwoFactorOtp({ code: completedCode, ...trust })\n      return\n    }\n\n    verifyTotp({ code: completedCode, ...trust })\n  }\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    const trust = trustDeviceEnabled ? { trustDevice } : {}\n\n    if (method === \"backup\") {\n      const formData = new FormData(e.currentTarget)\n      verifyBackupCode({\n        code: (formData.get(\"backupCode\") as string).trim(),\n        ...trust\n      })\n      return\n    }\n\n    verifyCode(code)\n  }\n\n  const description =\n    method === \"backup\"\n      ? twoFactorLocalization.backupCodeDescription\n      : method === \"otp\"\n        ? twoFactorLocalization.emailedCodeDescription\n        : twoFactorLocalization.authenticatorCodeDescription\n\n  const alternatives: { key: ChallengeMethod; label: string }[] = [\n    ...(method !== \"totp\" && methods.includes(\"totp\")\n      ? [\n          {\n            key: \"totp\" as const,\n            label: twoFactorLocalization.useAuthenticator\n          }\n        ]\n      : []),\n    ...(method !== \"otp\" && methods.includes(\"otp\")\n      ? [{ key: \"otp\" as const, label: twoFactorLocalization.useEmailedCode }]\n      : []),\n    ...(method !== \"backup\" && backupCodesEnabled\n      ? [{ key: \"backup\" as const, label: twoFactorLocalization.useBackupCode }]\n      : [])\n  ]\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">\n          {twoFactorLocalization.twoFactor}\n        </CardTitle>\n\n        <CardDescription>{description}</CardDescription>\n      </CardHeader>\n\n      <CardContent>\n        <form onSubmit={handleSubmit}>\n          <FieldGroup>\n            {method === \"backup\" ? (\n              <Field>\n                <FieldLabel htmlFor=\"backupCode\">\n                  {twoFactorLocalization.backupCode}\n                </FieldLabel>\n\n                <Input\n                  id=\"backupCode\"\n                  name=\"backupCode\"\n                  autoComplete=\"one-time-code\"\n                  autoFocus\n                  required\n                  disabled={isPending}\n                />\n              </Field>\n            ) : (\n              <OtpField\n                autoFocus\n                disabled={isPending || needsOtpRequest}\n                label={\n                  method === \"otp\"\n                    ? twoFactorLocalization.emailedCode\n                    : twoFactorLocalization.authenticatorCode\n                }\n                length={codeLength}\n                name=\"code\"\n                value={code}\n                onChange={setCode}\n                onComplete={verifyCode}\n              />\n            )}\n\n            {trustDeviceEnabled && (\n              <Field orientation=\"horizontal\">\n                <Checkbox\n                  id=\"trustDevice\"\n                  name=\"trustDevice\"\n                  checked={trustDevice}\n                  disabled={isPending}\n                  onCheckedChange={(checked) =>\n                    setTrustDevice(checked === true)\n                  }\n                />\n\n                <FieldLabel htmlFor=\"trustDevice\" className=\"font-normal\">\n                  {twoFactorLocalization.trustDevice}\n                </FieldLabel>\n              </Field>\n            )}\n\n            <div className=\"flex flex-col gap-3\">\n              {needsOtpRequest ? (\n                <Button\n                  type=\"button\"\n                  disabled={isSendingOtp}\n                  onClick={() => sendTwoFactorOtp()}\n                >\n                  {isSendingOtp && <Spinner />}\n\n                  {twoFactorLocalization.sendEmailCode}\n                </Button>\n              ) : (\n                <Button\n                  type=\"submit\"\n                  disabled={\n                    isPending ||\n                    (method !== \"backup\" && code.length !== codeLength)\n                  }\n                >\n                  {isPending && <Spinner />}\n\n                  {twoFactorLocalization.verify}\n                </Button>\n              )}\n\n              {method === \"otp\" && otpRequested && (\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  disabled={isPending || isCoolingDown}\n                  onClick={() => sendTwoFactorOtp()}\n                >\n                  {isCoolingDown\n                    ? localization.auth.resendIn.replace(\n                        \"{{seconds}}\",\n                        String(cooldown)\n                      )\n                    : localization.auth.resend}\n                </Button>\n              )}\n\n              {alternatives.map((alternative) => (\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  key={alternative.key}\n                  disabled={isPending}\n                  onClick={() => switchMethod(alternative.key)}\n                >\n                  {alternative.label}\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            <Link\n              href={`${basePaths.auth}/${viewPaths.auth.signIn}`}\n              className=\"underline underline-offset-4\"\n            >\n              {twoFactorLocalization.backToSignIn}\n            </Link>\n          </FieldDescription>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/two-factor-challenge.tsx"
    },
    {
      "path": "src/components/auth/two-factor/two-factor-settings.tsx",
      "content": "\"use client\"\n\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport { useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\nimport { cn } from \"@/lib/utils\"\nimport { DisableTwoFactorDialog } from \"./disable-two-factor-dialog\"\nimport { EnableTwoFactorDialog } from \"./enable-two-factor-dialog\"\nimport { RegenerateBackupCodesDialog } from \"./regenerate-backup-codes-dialog\"\n\nexport type TwoFactorSettingsProps = {\n  className?: string\n}\n\n/**\n * Security-settings card for enrolling in and managing two-factor auth.\n *\n * Reads `user.twoFactorEnabled` from the session — the field the Better Auth\n * two-factor plugin adds — so the card reflects enrollment without an extra\n * request.\n *\n * @param className - Additional CSS classes applied to the card.\n */\nexport function TwoFactorSettings({ className }: TwoFactorSettingsProps) {\n  const { authClient } = useAuth()\n  const {\n    backupCodes: backupCodesEnabled,\n    localization: twoFactorLocalization\n  } = useAuthPlugin(twoFactorPlugin)\n\n  const { data: session, isPending } = useSession(authClient)\n  const isEnabled = Boolean(\n    (session?.user as { twoFactorEnabled?: boolean } | undefined)\n      ?.twoFactorEnabled\n  )\n\n  const [enableOpen, setEnableOpen] = useState(false)\n  const [disableOpen, setDisableOpen] = useState(false)\n  const [regenerateOpen, setRegenerateOpen] = useState(false)\n\n  return (\n    <div className={cn(\"flex flex-col gap-3\", className)}>\n      <div className=\"flex items-end justify-between gap-3\">\n        <h2 className=\"truncate text-sm font-semibold\">\n          {twoFactorLocalization.twoFactor}\n        </h2>\n\n        <Button\n          className=\"shrink-0\"\n          size=\"sm\"\n          variant={isEnabled ? \"destructive\" : \"default\"}\n          disabled={isPending}\n          onClick={() =>\n            isEnabled ? setDisableOpen(true) : setEnableOpen(true)\n          }\n        >\n          {isEnabled\n            ? twoFactorLocalization.disableTwoFactor\n            : twoFactorLocalization.enableTwoFactor}\n        </Button>\n      </div>\n\n      <Card>\n        <CardContent className=\"flex flex-col gap-4\">\n          {isPending ? (\n            <Skeleton className=\"h-5 w-48\" />\n          ) : (\n            <p className=\"text-sm font-medium\">\n              {isEnabled\n                ? twoFactorLocalization.twoFactorEnabled\n                : twoFactorLocalization.twoFactorDisabled}\n            </p>\n          )}\n\n          <p className=\"text-muted-foreground text-sm\">\n            {twoFactorLocalization.twoFactorDescription}\n          </p>\n\n          {isEnabled && backupCodesEnabled && (\n            <Button\n              className=\"self-start\"\n              size=\"sm\"\n              variant=\"outline\"\n              onClick={() => setRegenerateOpen(true)}\n            >\n              {twoFactorLocalization.regenerateBackupCodes}\n            </Button>\n          )}\n        </CardContent>\n      </Card>\n\n      <EnableTwoFactorDialog open={enableOpen} onOpenChange={setEnableOpen} />\n      <DisableTwoFactorDialog\n        open={disableOpen}\n        onOpenChange={setDisableOpen}\n      />\n      <RegenerateBackupCodesDialog\n        open={regenerateOpen}\n        onOpenChange={setRegenerateOpen}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/two-factor-settings.tsx"
    },
    {
      "path": "src/components/auth/two-factor/enable-two-factor-dialog.tsx",
      "content": "\"use client\"\n\nimport { createQrCodeSvgData } from \"@better-auth-ui/core\"\nimport {\n  type TwoFactorAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useEnableTwoFactor,\n  useVerifyTotp\n} from \"@better-auth-ui/react\"\nimport { Check, Copy, ShieldCheck } from \"lucide-react\"\nimport {\n  type SyntheticEvent,\n  useEffect,\n  useMemo,\n  useRef,\n  useState\n} from \"react\"\nimport { toast } from \"sonner\"\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle\n} from \"@/components/ui/dialog\"\nimport { Field, FieldError, FieldLabel } 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 { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\nimport { useTwoFactorPasswordRequirement } from \"@/lib/auth/use-two-factor-password\"\nimport { OtpField } from \"../otp-field\"\nimport { BackupCodes } from \"./backup-codes\"\n\ntype EnrollmentStep = \"password\" | \"verify\" | \"backupCodes\"\n\nexport type EnableTwoFactorDialogProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\n/**\n * Three-step two-factor enrollment: confirm the password, scan the QR code\n * and verify a first code, then save the backup codes.\n *\n * Better Auth only marks two-factor as active once a TOTP code verifies, so\n * the dialog never closes on the enable call alone.\n *\n * @param open - Whether the dialog is open.\n * @param onOpenChange - Called when the dialog requests an open state change.\n */\nexport function EnableTwoFactorDialog({\n  open,\n  onOpenChange\n}: EnableTwoFactorDialogProps) {\n  const { authClient, localization } = useAuth()\n  const { codeLength, localization: twoFactorLocalization } =\n    useAuthPlugin(twoFactorPlugin)\n  const { isPending: isResolvingPasswordRequirement, requiresPassword } =\n    useTwoFactorPasswordRequirement()\n\n  const twoFactorClient = authClient as TwoFactorAuthClient\n\n  const [step, setStep] = useState<EnrollmentStep>(\"password\")\n  const [totpUri, setTotpUri] = useState(\"\")\n  const [backupCodes, setBackupCodes] = useState<string[]>([])\n  const [code, setCode] = useState(\"\")\n  const [setupKeyCopied, setSetupKeyCopied] = useState(false)\n  const copyResetTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n  const qrCode = useMemo(\n    () => (totpUri ? createQrCodeSvgData(totpUri) : null),\n    [totpUri]\n  )\n\n  // Manual entry fallback for authenticator apps that can't scan. The URI is\n  // an `otpauth://` URL, so the secret is just a query parameter.\n  const setupKey = useMemo(() => {\n    if (!totpUri) return null\n\n    try {\n      return new URL(totpUri).searchParams.get(\"secret\")\n    } catch {\n      return null\n    }\n  }, [totpUri])\n\n  useEffect(\n    () => () => {\n      if (copyResetTimeout.current !== null) {\n        clearTimeout(copyResetTimeout.current)\n      }\n    },\n    []\n  )\n\n  const copySetupKey = async () => {\n    if (!setupKey) return\n\n    try {\n      await navigator.clipboard.writeText(setupKey)\n      setSetupKeyCopied(true)\n\n      if (copyResetTimeout.current !== null) {\n        clearTimeout(copyResetTimeout.current)\n      }\n\n      copyResetTimeout.current = setTimeout(() => {\n        setSetupKeyCopied(false)\n        copyResetTimeout.current = null\n      }, 2000)\n    } catch {\n      toast.error(twoFactorLocalization.setupKeyCopyFailed)\n    }\n  }\n\n  const {\n    mutate: enableTwoFactor,\n    isPending: isEnabling,\n    reset: resetEnrollment\n  } = useEnableTwoFactor(twoFactorClient, {\n    onSuccess: (data) => {\n      setTotpUri(data.totpURI)\n      setBackupCodes(data.backupCodes)\n      setStep(\"verify\")\n    }\n  })\n\n  const { mutate: verifyTotp, isPending: isVerifying } = useVerifyTotp(\n    twoFactorClient,\n    {\n      onError: () => setCode(\"\"),\n      onSuccess: () => {\n        toast.success(twoFactorLocalization.twoFactorEnabled)\n        setStep(\"backupCodes\")\n      }\n    }\n  )\n\n  const isPending = isEnabling || isVerifying || isResolvingPasswordRequirement\n\n  const verifyCode = (completedCode: string) => {\n    if (isPending || step !== \"verify\" || completedCode.length !== codeLength) {\n      return\n    }\n\n    verifyTotp({ code: completedCode })\n  }\n\n  const handleOpenChange = (nextOpen: boolean) => {\n    onOpenChange(nextOpen)\n\n    if (!nextOpen) {\n      setStep(\"password\")\n      setTotpUri(\"\")\n      setBackupCodes([])\n      setCode(\"\")\n      setSetupKeyCopied(false)\n      if (copyResetTimeout.current !== null) {\n        clearTimeout(copyResetTimeout.current)\n        copyResetTimeout.current = null\n      }\n      // Clears the resolved TOTP URI and backup codes from the mutation cache.\n      resetEnrollment()\n    }\n  }\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    if (step === \"backupCodes\") {\n      handleOpenChange(false)\n      return\n    }\n\n    if (step === \"verify\") {\n      verifyCode(code)\n      return\n    }\n\n    const formData = new FormData(e.currentTarget)\n    const password = formData.get(\"password\") as string\n\n    enableTwoFactor(requiresPassword ? { password } : {})\n  }\n\n  const submitLabel =\n    step === \"backupCodes\"\n      ? twoFactorLocalization.done\n      : step === \"verify\"\n        ? twoFactorLocalization.verify\n        : twoFactorLocalization.enableTwoFactor\n\n  return (\n    <Dialog open={open} onOpenChange={handleOpenChange}>\n      <DialogContent>\n        <form onSubmit={handleSubmit} className=\"flex flex-col gap-6\">\n          <DialogHeader>\n            <DialogTitle>\n              <ShieldCheck />\n              {twoFactorLocalization.twoFactor}\n            </DialogTitle>\n\n            <DialogDescription>\n              {step === \"password\" && requiresPassword\n                ? twoFactorLocalization.passwordConfirmation\n                : step === \"verify\"\n                  ? twoFactorLocalization.scanQrCode\n                  : twoFactorLocalization.twoFactorDescription}\n            </DialogDescription>\n          </DialogHeader>\n\n          {step === \"password\" && requiresPassword && (\n            <Field>\n              <FieldLabel htmlFor=\"two-factor-password\">\n                {localization.auth.password}\n              </FieldLabel>\n\n              <Input\n                id=\"two-factor-password\"\n                name=\"password\"\n                type=\"password\"\n                autoComplete=\"current-password\"\n                autoFocus\n                required\n                placeholder={localization.auth.passwordPlaceholder}\n                disabled={isPending}\n              />\n\n              <FieldError />\n            </Field>\n          )}\n\n          {step === \"verify\" && (\n            <div className=\"flex flex-col items-center gap-4\">\n              {qrCode && (\n                <svg\n                  aria-hidden=\"true\"\n                  className=\"size-44 rounded-md border\"\n                  viewBox={`0 0 ${qrCode.size} ${qrCode.size}`}\n                >\n                  <path\n                    fill=\"white\"\n                    d={`M0 0h${qrCode.size}v${qrCode.size}H0z`}\n                  />\n                  <path\n                    fill=\"black\"\n                    d={qrCode.path}\n                    shapeRendering=\"crispEdges\"\n                  />\n                </svg>\n              )}\n\n              {setupKey && (\n                <Field className=\"w-full gap-1\">\n                  <FieldLabel\n                    className=\"text-muted-foreground text-xs\"\n                    htmlFor=\"two-factor-setup-key\"\n                  >\n                    {twoFactorLocalization.setupKey}\n                  </FieldLabel>\n\n                  <InputGroup>\n                    <InputGroupInput\n                      className=\"font-mono text-xs\"\n                      id=\"two-factor-setup-key\"\n                      readOnly\n                      value={setupKey}\n                    />\n\n                    <InputGroupAddon align=\"inline-end\">\n                      <InputGroupButton\n                        aria-label={\n                          setupKeyCopied\n                            ? twoFactorLocalization.setupKeyCopied\n                            : localization.settings.copyToClipboard\n                        }\n                        onClick={copySetupKey}\n                        size=\"icon-xs\"\n                      >\n                        {setupKeyCopied ? <Check /> : <Copy />}\n                      </InputGroupButton>\n                    </InputGroupAddon>\n                  </InputGroup>\n                </Field>\n              )}\n\n              <OtpField\n                autoFocus\n                className=\"w-full\"\n                disabled={isPending}\n                label={twoFactorLocalization.authenticatorCode}\n                length={codeLength}\n                name=\"code\"\n                value={code}\n                onChange={setCode}\n                onComplete={verifyCode}\n              />\n            </div>\n          )}\n\n          {step === \"backupCodes\" && <BackupCodes codes={backupCodes} />}\n\n          <DialogFooter>\n            {step !== \"backupCodes\" && (\n              <DialogClose\n                className={buttonVariants({ variant: \"outline\" })}\n                disabled={isPending}\n                type=\"button\"\n              >\n                {localization.settings.cancel}\n              </DialogClose>\n            )}\n\n            <Button\n              type=\"submit\"\n              disabled={\n                isPending || (step === \"verify\" && code.length !== codeLength)\n              }\n            >\n              {isPending && <Spinner />}\n\n              {submitLabel}\n            </Button>\n          </DialogFooter>\n        </form>\n      </DialogContent>\n    </Dialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/enable-two-factor-dialog.tsx"
    },
    {
      "path": "src/components/auth/two-factor/disable-two-factor-dialog.tsx",
      "content": "\"use client\"\n\nimport {\n  type TwoFactorAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useDisableTwoFactor\n} from \"@better-auth-ui/react\"\nimport { ShieldAlert } from \"lucide-react\"\nimport type { SyntheticEvent } from \"react\"\nimport { toast } from \"sonner\"\n\nimport {\n  AlertDialog,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogMedia,\n  AlertDialogTitle\n} from \"@/components/ui/alert-dialog\"\nimport { Button } from \"@/components/ui/button\"\nimport { Field, FieldError, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\nimport { useTwoFactorPasswordRequirement } from \"@/lib/auth/use-two-factor-password\"\n\nexport type DisableTwoFactorDialogProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\n/**\n * Confirm turning two-factor off.\n *\n * @param open - Whether the dialog is open.\n * @param onOpenChange - Called when the dialog requests an open state change.\n */\nexport function DisableTwoFactorDialog({\n  open,\n  onOpenChange\n}: DisableTwoFactorDialogProps) {\n  const { authClient, localization } = useAuth()\n  const { localization: twoFactorLocalization } = useAuthPlugin(twoFactorPlugin)\n  const { isPending: isResolvingPasswordRequirement, requiresPassword } =\n    useTwoFactorPasswordRequirement()\n\n  const { mutate: disableTwoFactor, isPending: isDisabling } =\n    useDisableTwoFactor(authClient as TwoFactorAuthClient, {\n      onSuccess: () => {\n        toast.success(twoFactorLocalization.twoFactorDisabled)\n        onOpenChange(false)\n      }\n    })\n\n  const isPending = isDisabling || isResolvingPasswordRequirement\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    const formData = new FormData(e.currentTarget)\n    const password = formData.get(\"password\") as string\n\n    disableTwoFactor(requiresPassword ? { password } : {})\n  }\n\n  return (\n    <AlertDialog open={open} onOpenChange={onOpenChange}>\n      <AlertDialogContent>\n        <form onSubmit={handleSubmit} className=\"flex flex-col gap-6\">\n          <AlertDialogHeader>\n            <AlertDialogMedia className=\"bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive\">\n              <ShieldAlert />\n            </AlertDialogMedia>\n\n            <AlertDialogTitle>\n              {twoFactorLocalization.disableTwoFactor}\n            </AlertDialogTitle>\n\n            <AlertDialogDescription>\n              {requiresPassword\n                ? twoFactorLocalization.passwordConfirmation\n                : twoFactorLocalization.twoFactorDescription}\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n\n          {requiresPassword && (\n            <Field>\n              <FieldLabel htmlFor=\"disable-two-factor-password\">\n                {localization.auth.password}\n              </FieldLabel>\n\n              <Input\n                id=\"disable-two-factor-password\"\n                name=\"password\"\n                type=\"password\"\n                autoComplete=\"current-password\"\n                autoFocus\n                required\n                placeholder={localization.auth.passwordPlaceholder}\n                disabled={isPending}\n              />\n\n              <FieldError />\n            </Field>\n          )}\n\n          <AlertDialogFooter>\n            <AlertDialogCancel disabled={isPending}>\n              {localization.settings.cancel}\n            </AlertDialogCancel>\n\n            <Button type=\"submit\" variant=\"destructive\" disabled={isPending}>\n              {isPending && <Spinner />}\n\n              {twoFactorLocalization.disableTwoFactor}\n            </Button>\n          </AlertDialogFooter>\n        </form>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/disable-two-factor-dialog.tsx"
    },
    {
      "path": "src/components/auth/two-factor/regenerate-backup-codes-dialog.tsx",
      "content": "\"use client\"\n\nimport {\n  type TwoFactorAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useGenerateBackupCodes\n} from \"@better-auth-ui/react\"\nimport { KeyRound } from \"lucide-react\"\nimport { type SyntheticEvent, useState } from \"react\"\nimport { toast } from \"sonner\"\n\nimport {\n  AlertDialog,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogMedia,\n  AlertDialogTitle\n} from \"@/components/ui/alert-dialog\"\nimport { Button } from \"@/components/ui/button\"\nimport { Field, FieldError, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\nimport { useTwoFactorPasswordRequirement } from \"@/lib/auth/use-two-factor-password\"\nimport { BackupCodes } from \"./backup-codes\"\n\nexport type RegenerateBackupCodesDialogProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\n/**\n * Replace the existing backup codes with a fresh set.\n *\n * The new codes are shown once, in component state only — closing the dialog\n * is the point of no return, which is why the copy button sits right there.\n *\n * @param open - Whether the dialog is open.\n * @param onOpenChange - Called when the dialog requests an open state change.\n */\nexport function RegenerateBackupCodesDialog({\n  open,\n  onOpenChange\n}: RegenerateBackupCodesDialogProps) {\n  const { authClient, localization } = useAuth()\n  const { localization: twoFactorLocalization } = useAuthPlugin(twoFactorPlugin)\n  const { isPending: isResolvingPasswordRequirement, requiresPassword } =\n    useTwoFactorPasswordRequirement()\n\n  const [codes, setCodes] = useState<string[]>([])\n\n  const {\n    mutate: generateBackupCodes,\n    isPending: isGenerating,\n    reset: resetGeneration\n  } = useGenerateBackupCodes(authClient as TwoFactorAuthClient, {\n    onSuccess: (data) => {\n      setCodes(data.backupCodes)\n      toast.success(twoFactorLocalization.backupCodesRegenerated)\n    }\n  })\n\n  const isPending = isGenerating || isResolvingPasswordRequirement\n\n  const handleOpenChange = (nextOpen: boolean) => {\n    onOpenChange(nextOpen)\n\n    if (!nextOpen) {\n      setCodes([])\n      // Clears the resolved backup codes from the mutation cache.\n      resetGeneration()\n    }\n  }\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    if (codes.length) {\n      handleOpenChange(false)\n      return\n    }\n\n    const formData = new FormData(e.currentTarget)\n    const password = formData.get(\"password\") as string\n\n    generateBackupCodes(requiresPassword ? { password } : {})\n  }\n\n  return (\n    <AlertDialog open={open} onOpenChange={handleOpenChange}>\n      <AlertDialogContent>\n        <form onSubmit={handleSubmit} className=\"flex flex-col gap-6\">\n          <AlertDialogHeader>\n            <AlertDialogMedia>\n              <KeyRound />\n            </AlertDialogMedia>\n\n            <AlertDialogTitle>\n              {twoFactorLocalization.backupCodes}\n            </AlertDialogTitle>\n\n            <AlertDialogDescription>\n              {codes.length || !requiresPassword\n                ? twoFactorLocalization.backupCodesDescription\n                : twoFactorLocalization.passwordConfirmation}\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n\n          {codes.length ? (\n            <BackupCodes codes={codes} />\n          ) : (\n            requiresPassword && (\n              <Field>\n                <FieldLabel htmlFor=\"regenerate-backup-codes-password\">\n                  {localization.auth.password}\n                </FieldLabel>\n\n                <Input\n                  id=\"regenerate-backup-codes-password\"\n                  name=\"password\"\n                  type=\"password\"\n                  autoComplete=\"current-password\"\n                  autoFocus\n                  required\n                  placeholder={localization.auth.passwordPlaceholder}\n                  disabled={isPending}\n                />\n\n                <FieldError />\n              </Field>\n            )\n          )}\n\n          <AlertDialogFooter>\n            {!codes.length && (\n              <AlertDialogCancel disabled={isPending}>\n                {localization.settings.cancel}\n              </AlertDialogCancel>\n            )}\n\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending && <Spinner />}\n\n              {codes.length\n                ? twoFactorLocalization.done\n                : twoFactorLocalization.regenerateBackupCodes}\n            </Button>\n          </AlertDialogFooter>\n        </form>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/regenerate-backup-codes-dialog.tsx"
    },
    {
      "path": "src/components/auth/two-factor/backup-codes.tsx",
      "content": "\"use client\"\n\nimport {\n  downloadTextFile,\n  formatBackupCodesText,\n  printTextFile\n} from \"@better-auth-ui/core\"\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport { Copy, Download, Printer } from \"lucide-react\"\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { twoFactorPlugin } from \"@/lib/auth/two-factor-plugin\"\n\nexport type BackupCodesProps = {\n  codes: string[]\n}\n\n/**\n * One-time display of freshly generated backup codes.\n *\n * The codes are never persisted anywhere on the client. They live in the\n * calling component's state until the dialog closes, so the save actions\n * matter.\n *\n * @param codes - Backup codes returned by enable or regenerate.\n */\nexport function BackupCodes({ codes }: BackupCodesProps) {\n  const { localization } = useAuth()\n  const { localization: twoFactorLocalization } = useAuthPlugin(twoFactorPlugin)\n  const getBackupCodesText = () =>\n    formatBackupCodesText(codes, twoFactorLocalization, window.location.origin)\n\n  // Clipboard writes reject on insecure origins and when the user denies the\n  // permission, so the codes stay on screen and the toast tells them to copy\n  // by hand rather than leaving a rejected promise behind.\n  const copyCodes = async () => {\n    try {\n      await navigator.clipboard.writeText(getBackupCodesText())\n      toast.success(twoFactorLocalization.backupCodesCopied)\n    } catch {\n      toast.error(twoFactorLocalization.backupCodesCopyFailed)\n    }\n  }\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <p className=\"text-muted-foreground text-sm\">\n        {twoFactorLocalization.backupCodesDescription}\n      </p>\n\n      <ul className=\"grid grid-cols-2 gap-2 rounded-md border bg-muted/40 p-4 font-mono text-sm\">\n        {codes.map((backupCode) => (\n          <li className=\"tracking-wide\" key={backupCode}>\n            {backupCode}\n          </li>\n        ))}\n      </ul>\n\n      <div className=\"flex flex-wrap gap-2\">\n        <Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={copyCodes}>\n          <Copy data-icon=\"inline-start\" />\n          {localization.settings.copyToClipboard}\n        </Button>\n\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          variant=\"outline\"\n          onClick={() =>\n            downloadTextFile(getBackupCodesText(), \"backup-codes.txt\")\n          }\n        >\n          <Download data-icon=\"inline-start\" />\n          {twoFactorLocalization.downloadBackupCodes}\n        </Button>\n\n        <Button\n          type=\"button\"\n          size=\"sm\"\n          variant=\"outline\"\n          onClick={() => printTextFile(getBackupCodesText())}\n        >\n          <Printer data-icon=\"inline-start\" />\n          {twoFactorLocalization.printBackupCodes}\n        </Button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/two-factor/backup-codes.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"
}