{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "username",
  "title": "Username",
  "description": "Username plugin: username-based sign-in form and availability checking field. Registers a sign-in view that accepts both username and email, and a username field with real-time availability checking rendered on the sign-up form.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "@tanstack/react-pacer",
    "better-auth",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "checkbox",
    "field",
    "input",
    "input-group",
    "separator",
    "sonner",
    "spinner",
    "https://better-auth-ui.com/r/radix-nova/additional-field.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/username-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  usernamePlugin as coreUsernamePlugin,\n  type UsernamePluginOptions\n} from \"@better-auth-ui/core/plugins\"\n\nimport { SignInUsername } from \"@/components/auth/username/sign-in-username\"\nimport { UsernameField } from \"@/components/auth/username/username-field\"\n\nexport const usernamePlugin = createAuthPlugin(\n  coreUsernamePlugin.id,\n  (options: UsernamePluginOptions = {}) => {\n    const core = coreUsernamePlugin(options)\n\n    return {\n      ...core,\n      additionalFields: core.additionalFields?.map((field) =>\n        field.name === \"username\"\n          ? {\n              ...field,\n              render: UsernameField\n            }\n          : field\n      ),\n      views: {\n        auth: { signIn: SignInUsername }\n      }\n    }\n  }\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/username-plugin.ts"
    },
    {
      "path": "src/components/auth/username/username-field.tsx",
      "content": "\"use client\"\n\nimport {\n  type UsernameAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useIsUsernameAvailable\n} from \"@better-auth-ui/react\"\nimport { useDebouncer } from \"@tanstack/react-pacer\"\nimport { Check, X } from \"lucide-react\"\nimport { useState } from \"react\"\nimport type { AdditionalFieldProps } from \"@/components/auth/additional-field\"\nimport { Field, FieldError, FieldLabel } from \"@/components/ui/field\"\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput\n} from \"@/components/ui/input-group\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { usernamePlugin } from \"@/lib/auth/username-plugin\"\n\n/**\n * Renderer for the `username` additional field. Owns availability checking,\n * length limits, and visual indicators. `isInvalid` reflects only browser\n * validation (minLength, required, etc.) — availability feedback is shown\n * via the icon and `aria-label` without affecting the field's invalid state.\n */\nexport function UsernameField({\n  name,\n  field,\n  isPending\n}: AdditionalFieldProps) {\n  const { authClient, localization: authLocalization } = useAuth()\n  const {\n    localization,\n    minUsernameLength,\n    maxUsernameLength,\n    isUsernameAvailable: checkAvailability,\n    usernamePrefix\n  } = useAuthPlugin(usernamePlugin)\n\n  const currentUsername = String(field.defaultValue ?? \"\")\n  const [value, setValue] = useState(currentUsername)\n  const [error, setError] = useState<string>()\n\n  const {\n    mutate: requestAvailability,\n    data: availability,\n    error: availabilityError,\n    reset: resetAvailability\n  } = useIsUsernameAvailable(authClient as UsernameAuthClient, {\n    onError: () => {}\n  })\n\n  const debouncer = useDebouncer(\n    (next: string) => {\n      const trimmed = next.trim()\n      if (!trimmed || trimmed === currentUsername) {\n        resetAvailability()\n        return\n      }\n\n      requestAvailability({ username: trimmed })\n    },\n    { wait: 500 }\n  )\n\n  function handleChange(next: string) {\n    setValue(next)\n    setError(undefined)\n    resetAvailability()\n\n    if (checkAvailability) {\n      debouncer.maybeExecute(next)\n    }\n  }\n\n  const isCheckingAvailability =\n    !!checkAvailability && !!value.trim() && value.trim() !== currentUsername\n\n  return (\n    <Field data-invalid={!!error}>\n      <FieldLabel htmlFor={name}>{field.label}</FieldLabel>\n\n      <InputGroup>\n        {usernamePrefix && (\n          <InputGroupAddon align=\"inline-start\">\n            {usernamePrefix}\n          </InputGroupAddon>\n        )}\n\n        <InputGroupInput\n          id={name}\n          name={name}\n          type=\"text\"\n          autoComplete=\"username\"\n          minLength={minUsernameLength}\n          maxLength={maxUsernameLength}\n          disabled={isPending}\n          required={field.required}\n          readOnly={field.readOnly}\n          value={value}\n          onChange={(e) => handleChange(e.target.value)}\n          onInvalid={(e) => {\n            e.preventDefault()\n            const el = e.target as HTMLInputElement\n            const msg = el.validity.valueMissing\n              ? authLocalization.auth.fieldRequired\n              : el.validity.tooShort\n                ? authLocalization.auth.tooShort.replace(\n                    \"{{min}}\",\n                    String(minUsernameLength)\n                  )\n                : authLocalization.auth.tooLong.replace(\n                    \"{{max}}\",\n                    String(maxUsernameLength)\n                  )\n            setError(msg)\n          }}\n          aria-invalid={!!error}\n          placeholder={field.placeholder}\n        />\n\n        {isCheckingAvailability && (\n          <InputGroupAddon\n            align=\"inline-end\"\n            aria-label={\n              availability?.available\n                ? localization.usernameAvailable\n                : availability?.available === false\n                  ? localization.usernameTaken\n                  : undefined\n            }\n          >\n            {availability?.available ? (\n              <Check className=\"size-4 text-foreground\" />\n            ) : availabilityError || availability?.available === false ? (\n              <X className=\"size-4 text-destructive\" />\n            ) : (\n              <Spinner />\n            )}\n          </InputGroupAddon>\n        )}\n      </InputGroup>\n\n      <FieldError>{error}</FieldError>\n    </Field>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/username/username-field.tsx"
    },
    {
      "path": "src/components/auth/username/sign-in-username.tsx",
      "content": "\"use client\"\n\nimport { authMutationKeys } from \"@better-auth-ui/core\"\nimport {\n  AuthPrompts,\n  type UsernameAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useFetchOptions,\n  useSignInEmail,\n  useSignInUsername\n} from \"@better-auth-ui/react\"\nimport { useIsMutating } from \"@tanstack/react-query\"\nimport { Eye, EyeOff } from \"lucide-react\"\nimport { type SyntheticEvent, useState } from \"react\"\nimport {\n  ProviderButtons,\n  type SocialLayout\n} from \"@/components/auth/provider-buttons\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\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 {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput\n} from \"@/components/ui/input-group\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { useSignInContinuation } from \"@/lib/auth/use-sign-in-continuation\"\nimport { usernamePlugin } from \"@/lib/auth/username-plugin\"\nimport { cn } from \"@/lib/utils\"\nimport { LastUsedBadge } from \"../last-login-method/last-used-badge\"\n\nexport type SignInUsernameProps = {\n  className?: string\n  socialLayout?: SocialLayout\n  socialPosition?: \"top\" | \"bottom\"\n}\n\nfunction isEmail(value: string): boolean {\n  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)\n}\n\n/**\n * Render the username-based sign-in form. Identical to the built-in `<SignIn>`\n * design but routes non-email inputs through `signInUsername` instead of\n * `signInEmail`.\n */\nexport function SignInUsername({\n  className,\n  socialLayout,\n  socialPosition = \"bottom\"\n}: SignInUsernameProps) {\n  const {\n    authClient,\n    basePaths,\n    emailAndPassword,\n    localization,\n    plugins,\n    socialProviders,\n    viewPaths,\n    navigate,\n    Link\n  } = useAuth()\n\n  const { fetchOptions, resetFetchOptions } = useFetchOptions()\n  const continueSignIn = useSignInContinuation()\n\n  const { localization: usernameLocalization } = useAuthPlugin(usernamePlugin)\n\n  const [password, setPassword] = useState(\"\")\n\n  const { mutate: signInEmail, isPending: isSignInEmailPending } =\n    useSignInEmail(authClient, {\n      onError: (error, { email }) => {\n        setPassword(\"\")\n\n        if (error.error?.code === \"EMAIL_NOT_VERIFIED\") {\n          sessionStorage.setItem(\"better-auth-ui.verify-email\", email)\n          navigate({\n            to: `${basePaths.auth}/${viewPaths.auth.verifyEmail}`\n          })\n        }\n\n        resetFetchOptions()\n      },\n      onSuccess: (data) => {\n        sessionStorage.removeItem(\"better-auth-ui.verify-email\")\n        continueSignIn(data)\n      }\n    })\n\n  const { mutate: signInUsername, isPending: isSignInUsernamePending } =\n    useSignInUsername(authClient as UsernameAuthClient, {\n      onError: (error) => {\n        setPassword(\"\")\n\n        if (error.error?.code === \"EMAIL_NOT_VERIFIED\") {\n          sessionStorage.removeItem(\"better-auth-ui.verify-email\")\n\n          navigate({\n            to: `${basePaths.auth}/${viewPaths.auth.verifyEmail}`\n          })\n        }\n\n        resetFetchOptions()\n      },\n      onSuccess: (data) => {\n        sessionStorage.removeItem(\"better-auth-ui.verify-email\")\n        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\n  const isSignInPending = isSignInEmailPending || isSignInUsernamePending\n\n  const Captcha = plugins.find(\n    (plugin) => plugin.captchaComponent\n  )?.captchaComponent\n\n  const [isPasswordVisible, setIsPasswordVisible] = useState(false)\n\n  const [fieldErrors, setFieldErrors] = useState<{\n    email?: string\n    password?: string\n  }>({})\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    const formData = new FormData(e.currentTarget)\n    const email = formData.get(\"email\") as string\n    const rememberMe = formData.get(\"rememberMe\") === \"on\"\n\n    if (isEmail(email)) {\n      signInEmail({\n        email,\n        password,\n        ...(emailAndPassword?.rememberMe ? { rememberMe } : {}),\n        fetchOptions\n      })\n    } else {\n      signInUsername({\n        username: email,\n        password,\n        ...(emailAndPassword?.rememberMe ? { rememberMe } : {}),\n        fetchOptions\n      })\n    }\n  }\n\n  const showSeparator =\n    emailAndPassword?.enabled && socialProviders && socialProviders.length > 0\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <AuthPrompts view=\"signIn\" />\n      <CardHeader>\n        <CardTitle className=\"text-xl font-semibold\">\n          {localization.auth.signIn}\n        </CardTitle>\n      </CardHeader>\n\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {socialPosition === \"top\" && (\n            <>\n              {socialProviders && socialProviders.length > 0 && (\n                <ProviderButtons socialLayout={socialLayout} view=\"signIn\" />\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          {emailAndPassword?.enabled && (\n            <form onSubmit={handleSubmit}>\n              <FieldGroup>\n                <Field data-invalid={!!fieldErrors.email}>\n                  <FieldLabel htmlFor=\"email\">\n                    {usernameLocalization.username}\n                  </FieldLabel>\n\n                  <Input\n                    id=\"email\"\n                    name=\"email\"\n                    type=\"text\"\n                    autoComplete=\"username\"\n                    placeholder={\n                      usernameLocalization.usernameOrEmailPlaceholder\n                    }\n                    required\n                    disabled={isPending}\n                    onChange={() => {\n                      setFieldErrors((prev) => ({\n                        ...prev,\n                        email: undefined\n                      }))\n                    }}\n                    onInvalid={(e) => {\n                      e.preventDefault()\n\n                      setFieldErrors((prev) => ({\n                        ...prev,\n                        email: localization.auth.fieldRequired\n                      }))\n                    }}\n                    aria-invalid={!!fieldErrors.email}\n                  />\n\n                  <FieldError>{fieldErrors.email}</FieldError>\n                </Field>\n\n                <Field data-invalid={!!fieldErrors.password}>\n                  <FieldLabel htmlFor=\"password\">\n                    {localization.auth.password}\n                  </FieldLabel>\n\n                  <InputGroup>\n                    <InputGroupInput\n                      id=\"password\"\n                      name=\"password\"\n                      type={isPasswordVisible ? \"text\" : \"password\"}\n                      autoComplete=\"current-password\"\n                      value={password}\n                      onChange={(e) => {\n                        setPassword(e.target.value)\n\n                        setFieldErrors((prev) => ({\n                          ...prev,\n                          password: undefined\n                        }))\n                      }}\n                      placeholder={localization.auth.passwordPlaceholder}\n                      required\n                      minLength={emailAndPassword?.minPasswordLength}\n                      maxLength={emailAndPassword?.maxPasswordLength}\n                      disabled={isPending}\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) => ({\n                          ...prev,\n                          password: msg\n                        }))\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={() => {\n                          setIsPasswordVisible((visible) => !visible)\n                        }}\n                      >\n                        {isPasswordVisible ? <EyeOff /> : <Eye />}\n                      </InputGroupButton>\n                    </InputGroupAddon>\n                  </InputGroup>\n\n                  <FieldError>{fieldErrors.password}</FieldError>\n                </Field>\n\n                {emailAndPassword.rememberMe && (\n                  <Field className=\"my-1\">\n                    <div className=\"flex items-center gap-3\">\n                      <Checkbox\n                        id=\"rememberMe\"\n                        name=\"rememberMe\"\n                        disabled={isPending}\n                      />\n\n                      <FieldLabel\n                        htmlFor=\"rememberMe\"\n                        className=\"cursor-pointer text-sm font-normal\"\n                      >\n                        {localization.auth.rememberMe}\n                      </FieldLabel>\n                    </div>\n                  </Field>\n                )}\n\n                {Captcha && (\n                  <div className=\"flex justify-center\">{Captcha}</div>\n                )}\n\n                <div className=\"flex flex-col gap-3\">\n                  <Button\n                    type=\"submit\"\n                    className=\"relative overflow-visible\"\n                    disabled={isPending}\n                  >\n                    {isSignInPending && <Spinner />}\n\n                    {localization.auth.signIn}\n\n                    <LastUsedBadge method={[\"email\", \"username\"]} floating />\n                  </Button>\n\n                  {plugins.flatMap((plugin) =>\n                    (plugin.authButtons ?? []).map((AuthButton, index) => (\n                      <AuthButton\n                        key={`${plugin.id}-${index.toString()}`}\n                        view=\"signIn\"\n                      />\n                    ))\n                  )}\n                </div>\n              </FieldGroup>\n            </form>\n          )}\n\n          {socialPosition === \"bottom\" && (\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=\"signIn\" />\n              )}\n            </>\n          )}\n        </div>\n\n        <div className=\"flex flex-col gap-3 items-center w-full mt-4\">\n          {emailAndPassword?.enabled && emailAndPassword?.forgotPassword && (\n            <Link\n              href={`${basePaths.auth}/${viewPaths.auth.forgotPassword}`}\n              className=\"self-center text-sm underline-offset-4 hover:underline\"\n            >\n              {localization.auth.forgotPasswordLink}\n            </Link>\n          )}\n\n          {emailAndPassword?.enabled && (\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          )}\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/username/sign-in-username.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/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"
    }
  ],
  "type": "registry:component"
}