{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "phone-number",
  "title": "Phone Number",
  "description": "Phone-number plugin with verification-code and password sign-in, password recovery, and verified phone-number management.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "better-auth",
    "input-otp",
    "lucide-react"
  ],
  "registryDependencies": [
    "alert-dialog",
    "button",
    "card",
    "checkbox",
    "field",
    "https://better-auth-ui.com/r/radix-nova/account-settings.json",
    "input",
    "input-group",
    "input-otp",
    "skeleton",
    "sonner",
    "spinner"
  ],
  "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/phone-number-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  phoneNumberPlugin as corePhoneNumberPlugin,\n  type PhoneNumberPluginOptions\n} from \"@better-auth-ui/core/plugins\"\n\nimport { ChangePhoneNumber } from \"@/components/auth/phone-number/change-phone-number\"\nimport { ForgotPhoneNumberPassword } from \"@/components/auth/phone-number/forgot-phone-number-password\"\nimport { PhoneNumber } from \"@/components/auth/phone-number/phone-number\"\nimport { PhoneNumberButton } from \"@/components/auth/phone-number/phone-number-button\"\nimport { ResetPhoneNumberPassword } from \"@/components/auth/phone-number/reset-phone-number-password\"\n\nexport const phoneNumberPlugin = createAuthPlugin(\n  corePhoneNumberPlugin.id,\n  (options: PhoneNumberPluginOptions = {}) => {\n    const plugin = corePhoneNumberPlugin(options)\n    const hasSignIn = plugin.signIn || plugin.passwordSignIn\n\n    return {\n      ...plugin,\n      authButtons: hasSignIn ? [PhoneNumberButton] : [],\n      views: {\n        auth: {\n          ...(hasSignIn && { phoneNumber: PhoneNumber }),\n          ...(plugin.passwordReset && {\n            phoneNumberForgotPassword: ForgotPhoneNumberPassword,\n            phoneNumberResetPassword: ResetPhoneNumberPassword\n          })\n        }\n      },\n      ...(hasSignIn && {\n        fallbackViews: { auth: { signIn: PhoneNumber } }\n      }),\n      accountCards: plugin.changePhoneNumber ? [ChangePhoneNumber] : []\n    }\n  }\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/phone-number-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/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/phone-number/phone-number.tsx",
      "content": "\"use client\"\n\nimport { authMutationKeys } from \"@better-auth-ui/core\"\nimport {\n  AuthPrompts,\n  type PhoneNumberAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useFetchOptions,\n  useSendPhoneNumberOtp,\n  useSignInPhoneNumber,\n  useVerifyPhoneNumber\n} from \"@better-auth-ui/react\"\nimport { useIsMutating } from \"@tanstack/react-query\"\nimport { Eye, EyeOff } from \"lucide-react\"\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 { 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 { phoneNumberPlugin } from \"@/lib/auth/phone-number-plugin\"\nimport { useResendCooldown } from \"@/lib/auth/use-resend-cooldown\"\nimport { useSignInContinuation } from \"@/lib/auth/use-sign-in-continuation\"\nimport { cn } from \"@/lib/utils\"\nimport { OtpField } from \"../otp-field\"\nimport { ProviderButtons, type SocialLayout } from \"../provider-buttons\"\n\ntype PhoneNumberMode = \"code\" | \"password\"\n\nexport type PhoneNumberProps = {\n  className?: string\n  socialLayout?: SocialLayout\n  socialPosition?: \"top\" | \"bottom\"\n}\n\n/** Sign in with either a phone verification code or a phone and password. */\nexport function PhoneNumber({\n  className,\n  socialLayout,\n  socialPosition = \"bottom\"\n}: PhoneNumberProps) {\n  const {\n    authClient,\n    basePaths,\n    emailAndPassword,\n    localization,\n    plugins,\n    socialProviders,\n    viewPaths,\n    Link\n  } = useAuth()\n  const {\n    localization: phoneLocalization,\n    otpLength,\n    passwordReset,\n    passwordSignIn,\n    signIn,\n    viewPaths: phoneNumberViewPaths\n  } = useAuthPlugin(phoneNumberPlugin)\n  const phoneClient = authClient as PhoneNumberAuthClient\n  const { fetchOptions, resetFetchOptions } = useFetchOptions()\n  const continueSignIn = useSignInContinuation()\n  const { cooldown, isCoolingDown, startCooldown } = useResendCooldown()\n  const [mode, setMode] = useState<PhoneNumberMode>(\n    signIn ? \"code\" : \"password\"\n  )\n  const [phoneNumber, setPhoneNumber] = useState(\"\")\n  const [password, setPassword] = useState(\"\")\n  const [code, setCode] = useState(\"\")\n  const [codeSent, setCodeSent] = useState(false)\n  const [isPasswordVisible, setIsPasswordVisible] = useState(false)\n  const [fieldErrors, setFieldErrors] = useState<{\n    phoneNumber?: string\n    password?: string\n  }>({})\n\n  const { mutate: sendOtp, isPending: isSending } = useSendPhoneNumberOtp(\n    phoneClient,\n    {\n      onError: () => resetFetchOptions(),\n      onSuccess: () => {\n        setCodeSent(true)\n        startCooldown()\n      }\n    }\n  )\n  const { mutate: verify, isPending: isVerifying } = useVerifyPhoneNumber(\n    phoneClient,\n    {\n      onError: () => setCode(\"\"),\n      onSuccess: (data) => continueSignIn(data)\n    }\n  )\n  const { mutate: signInWithPassword, isPending: isPasswordPending } =\n    useSignInPhoneNumber(phoneClient, {\n      onError: (error) => {\n        setPassword(\"\")\n        resetFetchOptions()\n\n        if (signIn && error.error?.code === \"PHONE_NUMBER_NOT_VERIFIED\") {\n          setMode(\"code\")\n          setCodeSent(true)\n          startCooldown()\n        }\n      },\n      onSuccess: (data) => continueSignIn(data)\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 =\n    signInMutating + signUpMutating > 0 || isSending || isVerifying\n  const canSwitchMode = signIn && passwordSignIn\n  const showProviders = !codeSent && Boolean(socialProviders?.length)\n  const showSeparator =\n    !codeSent &&\n    Boolean(\n      socialProviders?.length &&\n        (emailAndPassword?.enabled || signIn || passwordSignIn)\n    )\n  const Captcha = plugins.find(\n    (plugin) => plugin.captchaComponent\n  )?.captchaComponent\n\n  const sendCode = () =>\n    sendOtp({ phoneNumber, fetchOptions } as Parameters<typeof sendOtp>[0])\n  const verifyCode = (completedCode: string) => {\n    if (isPending || completedCode.length !== otpLength) return\n\n    verify({ phoneNumber, code: completedCode })\n  }\n  const switchMode = () => {\n    setMode((current) => (current === \"code\" ? \"password\" : \"code\"))\n    setCode(\"\")\n    setCodeSent(false)\n    setPassword(\"\")\n  }\n\n  const handleSubmit = (event: SyntheticEvent<HTMLFormElement>) => {\n    event.preventDefault()\n\n    if (mode === \"password\") {\n      const formData = new FormData(event.currentTarget)\n      signInWithPassword({\n        phoneNumber,\n        password,\n        ...(emailAndPassword?.rememberMe\n          ? { rememberMe: formData.get(\"rememberMe\") === \"on\" }\n          : {}),\n        fetchOptions\n      })\n      return\n    }\n\n    if (!codeSent) {\n      sendCode()\n      return\n    }\n\n    verifyCode(code)\n  }\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <AuthPrompts view=\"phoneNumber\" />\n      <CardHeader>\n        <CardTitle className=\"text-xl font-semibold\">\n          {localization.auth.signIn}\n        </CardTitle>\n\n        {codeSent && (\n          <CardDescription>\n            {phoneLocalization.codeSentTo.replace(\n              \"{{phoneNumber}}\",\n              phoneNumber\n            )}\n          </CardDescription>\n        )}\n      </CardHeader>\n\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {socialPosition === \"top\" && showProviders && (\n            <>\n              <ProviderButtons socialLayout={socialLayout} view=\"phoneNumber\" />\n              {showSeparator && (\n                <FieldSeparator className=\"m-0 flex items-center text-xs *:data-[slot=field-separator-content]:bg-card\">\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}\n                  label={phoneLocalization.phoneCode}\n                  length={otpLength}\n                  name=\"otp\"\n                  value={code}\n                  onChange={setCode}\n                  onComplete={verifyCode}\n                />\n              ) : (\n                <>\n                  <Field data-invalid={Boolean(fieldErrors.phoneNumber)}>\n                    <FieldLabel htmlFor=\"phoneNumber\">\n                      {phoneLocalization.phoneNumber}\n                    </FieldLabel>\n                    <Input\n                      id=\"phoneNumber\"\n                      name=\"phoneNumber\"\n                      type=\"tel\"\n                      autoComplete=\"tel\"\n                      inputMode=\"tel\"\n                      value={phoneNumber}\n                      placeholder={phoneLocalization.phoneNumberPlaceholder}\n                      required\n                      disabled={isPending}\n                      onChange={(event) => {\n                        setPhoneNumber(event.target.value)\n                        setFieldErrors((current) => ({\n                          ...current,\n                          phoneNumber: undefined\n                        }))\n                      }}\n                      onInvalid={(event) => {\n                        event.preventDefault()\n                        setFieldErrors((current) => ({\n                          ...current,\n                          phoneNumber: event.currentTarget.validationMessage\n                        }))\n                      }}\n                      aria-invalid={Boolean(fieldErrors.phoneNumber)}\n                    />\n                    <FieldError>{fieldErrors.phoneNumber}</FieldError>\n                  </Field>\n\n                  {mode === \"password\" && (\n                    <Field data-invalid={Boolean(fieldErrors.password)}>\n                      <FieldLabel htmlFor=\"phoneNumberPassword\">\n                        {localization.auth.password}\n                      </FieldLabel>\n                      <InputGroup>\n                        <InputGroupInput\n                          id=\"phoneNumberPassword\"\n                          name=\"password\"\n                          type={isPasswordVisible ? \"text\" : \"password\"}\n                          autoComplete=\"current-password\"\n                          value={password}\n                          placeholder={localization.auth.passwordPlaceholder}\n                          required\n                          minLength={emailAndPassword?.minPasswordLength}\n                          maxLength={emailAndPassword?.maxPasswordLength}\n                          disabled={isPending}\n                          onChange={(event) => {\n                            setPassword(event.target.value)\n                            setFieldErrors((current) => ({\n                              ...current,\n                              password: undefined\n                            }))\n                          }}\n                          onInvalid={(event) => {\n                            event.preventDefault()\n                            setFieldErrors((current) => ({\n                              ...current,\n                              password: event.currentTarget.validationMessage\n                            }))\n                          }}\n                          aria-invalid={Boolean(fieldErrors.password)}\n                        />\n                        <InputGroupAddon align=\"inline-end\">\n                          <InputGroupButton\n                            type=\"button\"\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                      <FieldError>{fieldErrors.password}</FieldError>\n                    </Field>\n                  )}\n\n                  {mode === \"password\" && emailAndPassword?.rememberMe && (\n                    <Field orientation=\"horizontal\">\n                      <Checkbox\n                        id=\"phoneNumberRememberMe\"\n                        name=\"rememberMe\"\n                        disabled={isPending}\n                      />\n                      <FieldLabel\n                        htmlFor=\"phoneNumberRememberMe\"\n                        className=\"cursor-pointer font-normal\"\n                      >\n                        {localization.auth.rememberMe}\n                      </FieldLabel>\n                    </Field>\n                  )}\n\n                  {Captcha && (\n                    <div className=\"flex justify-center\">{Captcha}</div>\n                  )}\n                </>\n              )}\n\n              <div className=\"flex flex-col gap-3\">\n                <Button\n                  type=\"submit\"\n                  disabled={\n                    isPending || (codeSent && code.length !== otpLength)\n                  }\n                >\n                  {(isSending || isVerifying || isPasswordPending) && (\n                    <Spinner />\n                  )}\n                  {mode === \"password\"\n                    ? localization.auth.signIn\n                    : codeSent\n                      ? phoneLocalization.verifyCode\n                      : phoneLocalization.sendCode}\n                </Button>\n\n                {codeSent ? (\n                  <>\n                    <Button\n                      type=\"button\"\n                      variant=\"outline\"\n                      disabled={isPending || 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                    <Button\n                      type=\"button\"\n                      variant=\"ghost\"\n                      disabled={isPending}\n                      onClick={() => {\n                        setCode(\"\")\n                        setCodeSent(false)\n                      }}\n                    >\n                      {phoneLocalization.useDifferentPhoneNumber}\n                    </Button>\n                  </>\n                ) : (\n                  <>\n                    {canSwitchMode && (\n                      <Button\n                        type=\"button\"\n                        variant=\"outline\"\n                        disabled={isPending}\n                        onClick={switchMode}\n                      >\n                        {mode === \"code\"\n                          ? phoneLocalization.usePassword\n                          : phoneLocalization.useVerificationCode}\n                      </Button>\n                    )}\n                    {plugins.flatMap((plugin) =>\n                      (plugin.authButtons ?? []).map((AuthButton) => (\n                        <AuthButton\n                          key={`${plugin.id}-${AuthButton.displayName ?? AuthButton.name}`}\n                          view=\"phoneNumber\"\n                        />\n                      ))\n                    )}\n                  </>\n                )}\n              </div>\n            </FieldGroup>\n          </form>\n\n          {socialPosition === \"bottom\" && showProviders && (\n            <>\n              {showSeparator && (\n                <FieldSeparator className=\"flex items-center text-xs *:data-[slot=field-separator-content]:bg-card\">\n                  {localization.auth.or}\n                </FieldSeparator>\n              )}\n              <ProviderButtons socialLayout={socialLayout} view=\"phoneNumber\" />\n            </>\n          )}\n        </div>\n\n        <div className=\"mt-4 flex w-full flex-col items-center gap-3\">\n          {mode === \"password\" && passwordReset && (\n            <Link\n              href={`${basePaths.auth}/${phoneNumberViewPaths.auth.phoneNumberForgotPassword}`}\n              className=\"text-sm underline-offset-4 hover:underline\"\n            >\n              {phoneLocalization.forgotPassword}\n            </Link>\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/phone-number/phone-number.tsx"
    },
    {
      "path": "src/components/auth/phone-number/phone-number-button.tsx",
      "content": "\"use client\"\n\nimport { type AuthView, authMutationKeys } from \"@better-auth-ui/core\"\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport { useIsMutating } from \"@tanstack/react-query\"\nimport { Lock, Smartphone } from \"lucide-react\"\n\nimport { buttonVariants } from \"@/components/ui/button\"\nimport { phoneNumberPlugin } from \"@/lib/auth/phone-number-plugin\"\nimport { cn } from \"@/lib/utils\"\n\nexport type PhoneNumberButtonProps = {\n  /** @remarks `AuthView` */\n  view?: AuthView\n}\n\n/** Switch between the configured phone-number view and password sign-in. */\nexport function PhoneNumberButton({ view }: PhoneNumberButtonProps) {\n  const { basePaths, emailAndPassword, localization, viewPaths, Link } =\n    useAuth()\n  const { localization: phoneLocalization, viewPaths: phoneNumberViewPaths } =\n    useAuthPlugin(phoneNumberPlugin)\n  const isPhoneNumberView = view === \"phoneNumber\"\n  const isPending =\n    useIsMutating({ mutationKey: authMutationKeys.signIn.all }) +\n      useIsMutating({ mutationKey: authMutationKeys.signUp.all }) >\n    0\n\n  if (isPhoneNumberView && !emailAndPassword?.enabled) return null\n\n  return (\n    <Link\n      href={`${basePaths.auth}/${\n        isPhoneNumberView\n          ? viewPaths.auth.signIn\n          : phoneNumberViewPaths.auth.phoneNumber\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 && \"pointer-events-none opacity-50\"\n      )}\n    >\n      {isPhoneNumberView ? (\n        <Lock data-icon=\"inline-start\" />\n      ) : (\n        <Smartphone data-icon=\"inline-start\" />\n      )}\n\n      {localization.auth.continueWith.replace(\n        \"{{provider}}\",\n        isPhoneNumberView\n          ? localization.auth.password\n          : phoneLocalization.phoneNumber\n      )}\n    </Link>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/phone-number/phone-number-button.tsx"
    },
    {
      "path": "src/components/auth/phone-number/forgot-phone-number-password.tsx",
      "content": "\"use client\"\n\nimport {\n  type PhoneNumberAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useFetchOptions,\n  useRequestPhoneNumberPasswordReset\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 { phoneNumberPlugin } from \"@/lib/auth/phone-number-plugin\"\nimport { cn } from \"@/lib/utils\"\n\nexport const PHONE_NUMBER_RESET_STORAGE_KEY =\n  \"better-auth-ui.phone-number-reset\"\n\nexport type ForgotPhoneNumberPasswordProps = {\n  className?: string\n}\n\n/** Request the verification code used to reset a phone credential password. */\nexport function ForgotPhoneNumberPassword({\n  className\n}: ForgotPhoneNumberPasswordProps) {\n  const { authClient, basePaths, localization, navigate, plugins, Link } =\n    useAuth()\n  const { localization: phoneLocalization, viewPaths: phoneNumberViewPaths } =\n    useAuthPlugin(phoneNumberPlugin)\n  const { fetchOptions, resetFetchOptions } = useFetchOptions()\n  const [fieldError, setFieldError] = useState<string>()\n  const { mutate: requestReset, isPending } =\n    useRequestPhoneNumberPasswordReset(authClient as PhoneNumberAuthClient, {\n      onError: () => resetFetchOptions(),\n      onSuccess: (_data, { phoneNumber }) => {\n        sessionStorage.setItem(PHONE_NUMBER_RESET_STORAGE_KEY, phoneNumber)\n        navigate({\n          to: `${basePaths.auth}/${phoneNumberViewPaths.auth.phoneNumberResetPassword}`\n        })\n      }\n    })\n  const Captcha = plugins.find(\n    (plugin) => plugin.captchaComponent\n  )?.captchaComponent\n\n  const handleSubmit = (event: SyntheticEvent<HTMLFormElement>) => {\n    event.preventDefault()\n    const formData = new FormData(event.currentTarget)\n    requestReset({\n      phoneNumber: String(formData.get(\"phoneNumber\") ?? \"\"),\n      fetchOptions\n    })\n  }\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">\n          {phoneLocalization.forgotPassword}\n        </CardTitle>\n      </CardHeader>\n      <CardContent>\n        <form onSubmit={handleSubmit}>\n          <FieldGroup>\n            <Field data-invalid={Boolean(fieldError)}>\n              <FieldLabel htmlFor=\"resetPhoneNumber\">\n                {phoneLocalization.phoneNumber}\n              </FieldLabel>\n              <Input\n                id=\"resetPhoneNumber\"\n                name=\"phoneNumber\"\n                type=\"tel\"\n                autoComplete=\"tel\"\n                inputMode=\"tel\"\n                placeholder={phoneLocalization.phoneNumberPlaceholder}\n                required\n                disabled={isPending}\n                onChange={() => setFieldError(undefined)}\n                onInvalid={(event) => {\n                  event.preventDefault()\n                  setFieldError(event.currentTarget.validationMessage)\n                }}\n                aria-invalid={Boolean(fieldError)}\n              />\n              <FieldError>{fieldError}</FieldError>\n            </Field>\n            {Captcha && <div className=\"flex justify-center\">{Captcha}</div>}\n            <Button type=\"submit\" disabled={isPending}>\n              {isPending && <Spinner />}\n              {phoneLocalization.sendCode}\n            </Button>\n          </FieldGroup>\n        </form>\n        <FieldDescription className=\"mt-4 text-center\">\n          {localization.auth.rememberYourPassword}{\" \"}\n          <Link\n            href={`${basePaths.auth}/${phoneNumberViewPaths.auth.phoneNumber}`}\n            className=\"underline underline-offset-4\"\n          >\n            {localization.auth.signIn}\n          </Link>\n        </FieldDescription>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/phone-number/forgot-phone-number-password.tsx"
    },
    {
      "path": "src/components/auth/phone-number/reset-phone-number-password.tsx",
      "content": "\"use client\"\n\nimport {\n  type PhoneNumberAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useResetPhoneNumberPassword\n} from \"@better-auth-ui/react\"\nimport { Eye, EyeOff } from \"lucide-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  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 { phoneNumberPlugin } from \"@/lib/auth/phone-number-plugin\"\nimport { cn } from \"@/lib/utils\"\nimport { OtpField } from \"../otp-field\"\nimport { useIsHydrated } from \"../use-is-hydrated\"\nimport { PHONE_NUMBER_RESET_STORAGE_KEY } from \"./forgot-phone-number-password\"\n\nexport type ResetPhoneNumberPasswordProps = {\n  className?: string\n}\n\n/** Reset a phone credential password with the code sent to the user. */\nexport function ResetPhoneNumberPassword({\n  className\n}: ResetPhoneNumberPasswordProps) {\n  const { authClient, basePaths, emailAndPassword, localization, navigate } =\n    useAuth()\n  const {\n    localization: phoneLocalization,\n    otpLength,\n    viewPaths: phoneNumberViewPaths\n  } = useAuthPlugin(phoneNumberPlugin)\n  const isHydrated = useIsHydrated()\n  const initialPhoneNumber =\n    (isHydrated && sessionStorage.getItem(PHONE_NUMBER_RESET_STORAGE_KEY)) || \"\"\n  const [phoneNumber, setPhoneNumber] = useState(initialPhoneNumber)\n  const [hasStoredPhoneNumber, setHasStoredPhoneNumber] = useState(\n    Boolean(initialPhoneNumber)\n  )\n  const [code, setCode] = useState(\"\")\n  const [password, setPassword] = useState(\"\")\n  const [isPasswordVisible, setIsPasswordVisible] = useState(false)\n  const [fieldErrors, setFieldErrors] = useState<{\n    phoneNumber?: string\n    password?: string\n  }>({})\n\n  useEffect(() => {\n    const stored = sessionStorage.getItem(PHONE_NUMBER_RESET_STORAGE_KEY) ?? \"\"\n    setPhoneNumber(stored)\n    setHasStoredPhoneNumber(Boolean(stored))\n  }, [])\n\n  const { mutate: resetPassword, isPending } = useResetPhoneNumberPassword(\n    authClient as PhoneNumberAuthClient,\n    {\n      onError: () => setCode(\"\"),\n      onSuccess: () => {\n        sessionStorage.removeItem(PHONE_NUMBER_RESET_STORAGE_KEY)\n        toast.success(localization.auth.passwordResetSuccess)\n        navigate({\n          to: `${basePaths.auth}/${phoneNumberViewPaths.auth.phoneNumber}`\n        })\n      }\n    }\n  )\n\n  const handleSubmit = (event: SyntheticEvent<HTMLFormElement>) => {\n    event.preventDefault()\n    const formData = new FormData(event.currentTarget)\n    const confirmPassword = String(formData.get(\"confirmPassword\") ?? \"\")\n\n    if (emailAndPassword?.confirmPassword && password !== confirmPassword) {\n      toast.error(localization.auth.passwordsDoNotMatch)\n      return\n    }\n    if (code.length !== otpLength) {\n      toast.error(\n        phoneLocalization.codeLengthMismatch.replace(\n          \"{{length}}\",\n          String(otpLength)\n        )\n      )\n      return\n    }\n\n    resetPassword({\n      phoneNumber,\n      otp: code,\n      newPassword: password\n    })\n  }\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">\n          {phoneLocalization.resetPassword}\n        </CardTitle>\n        {hasStoredPhoneNumber && (\n          <CardDescription>\n            {phoneLocalization.codeSentTo.replace(\n              \"{{phoneNumber}}\",\n              phoneNumber\n            )}\n          </CardDescription>\n        )}\n      </CardHeader>\n      <CardContent>\n        <form onSubmit={handleSubmit}>\n          <FieldGroup>\n            {!hasStoredPhoneNumber && (\n              <Field data-invalid={Boolean(fieldErrors.phoneNumber)}>\n                <FieldLabel htmlFor=\"passwordResetPhoneNumber\">\n                  {phoneLocalization.phoneNumber}\n                </FieldLabel>\n                <Input\n                  id=\"passwordResetPhoneNumber\"\n                  name=\"phoneNumber\"\n                  type=\"tel\"\n                  autoComplete=\"tel\"\n                  inputMode=\"tel\"\n                  value={phoneNumber}\n                  placeholder={phoneLocalization.phoneNumberPlaceholder}\n                  required\n                  disabled={isPending}\n                  onChange={(event) => {\n                    setPhoneNumber(event.target.value)\n                    setFieldErrors((current) => ({\n                      ...current,\n                      phoneNumber: undefined\n                    }))\n                  }}\n                  onInvalid={(event) => {\n                    event.preventDefault()\n                    setFieldErrors((current) => ({\n                      ...current,\n                      phoneNumber: event.currentTarget.validationMessage\n                    }))\n                  }}\n                  aria-invalid={Boolean(fieldErrors.phoneNumber)}\n                />\n                <FieldError>{fieldErrors.phoneNumber}</FieldError>\n              </Field>\n            )}\n\n            <OtpField\n              autoFocus={hasStoredPhoneNumber}\n              disabled={isPending}\n              label={phoneLocalization.phoneCode}\n              length={otpLength}\n              name=\"otp\"\n              value={code}\n              onChange={setCode}\n            />\n\n            <Field data-invalid={Boolean(fieldErrors.password)}>\n              <FieldLabel htmlFor=\"phoneNumberNewPassword\">\n                {localization.auth.newPassword}\n              </FieldLabel>\n              <InputGroup>\n                <InputGroupInput\n                  id=\"phoneNumberNewPassword\"\n                  name=\"password\"\n                  type={isPasswordVisible ? \"text\" : \"password\"}\n                  autoComplete=\"new-password\"\n                  value={password}\n                  placeholder={localization.auth.newPasswordPlaceholder}\n                  required\n                  minLength={emailAndPassword?.minPasswordLength}\n                  maxLength={emailAndPassword?.maxPasswordLength}\n                  disabled={isPending}\n                  onChange={(event) => {\n                    setPassword(event.target.value)\n                    setFieldErrors((current) => ({\n                      ...current,\n                      password: undefined\n                    }))\n                  }}\n                  onInvalid={(event) => {\n                    event.preventDefault()\n                    setFieldErrors((current) => ({\n                      ...current,\n                      password: event.currentTarget.validationMessage\n                    }))\n                  }}\n                  aria-invalid={Boolean(fieldErrors.password)}\n                />\n                <InputGroupAddon align=\"inline-end\">\n                  <InputGroupButton\n                    type=\"button\"\n                    size=\"icon-xs\"\n                    aria-label={\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              <FieldError>{fieldErrors.password}</FieldError>\n            </Field>\n\n            {emailAndPassword?.confirmPassword && (\n              <Field>\n                <FieldLabel htmlFor=\"phoneNumberConfirmPassword\">\n                  {localization.auth.confirmPassword}\n                </FieldLabel>\n                <Input\n                  id=\"phoneNumberConfirmPassword\"\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            <Button\n              type=\"submit\"\n              disabled={isPending || code.length !== otpLength}\n            >\n              {isPending && <Spinner />}\n              {phoneLocalization.resetPassword}\n            </Button>\n          </FieldGroup>\n        </form>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/phone-number/reset-phone-number-password.tsx"
    },
    {
      "path": "src/components/auth/phone-number/change-phone-number.tsx",
      "content": "\"use client\"\n\nimport {\n  type PhoneNumberAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useSendPhoneNumberOtp,\n  useSession,\n  useUpdateUser,\n  useVerifyPhoneNumber\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 { Card, CardContent, CardFooter } from \"@/components/ui/card\"\nimport {\n  Field,\n  FieldDescription,\n  FieldError,\n  FieldLabel\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { phoneNumberPlugin } from \"@/lib/auth/phone-number-plugin\"\nimport { cn } from \"@/lib/utils\"\nimport { OtpField } from \"../otp-field\"\nimport { RemovePhoneNumberDialog } from \"./remove-phone-number-dialog\"\n\ntype PhoneNumberUser = {\n  phoneNumber?: string | null\n}\n\nexport type ChangePhoneNumberProps = {\n  className?: string\n}\n\n/** Add, replace, or remove the authenticated user's verified phone number. */\nexport function ChangePhoneNumber({ className }: ChangePhoneNumberProps) {\n  const { authClient } = useAuth()\n  const { localization, otpLength } = useAuthPlugin(phoneNumberPlugin)\n  const phoneClient = authClient as PhoneNumberAuthClient\n  const { data: session } = useSession(phoneClient)\n  const currentPhoneNumber =\n    (session?.user as PhoneNumberUser | undefined)?.phoneNumber ?? \"\"\n  const [phoneNumber, setPhoneNumber] = useState(\"\")\n  const [code, setCode] = useState(\"\")\n  const [codeSent, setCodeSent] = useState(false)\n  const [fieldError, setFieldError] = useState<string>()\n\n  useEffect(() => {\n    if (session) setPhoneNumber(currentPhoneNumber)\n  }, [currentPhoneNumber, session])\n\n  const { mutate: sendOtp, isPending: isSending } = useSendPhoneNumberOtp(\n    phoneClient,\n    { onSuccess: () => setCodeSent(true) }\n  )\n  const { mutate: verify, isPending: isVerifying } = useVerifyPhoneNumber(\n    phoneClient,\n    {\n      onError: () => setCode(\"\"),\n      onSuccess: () => {\n        setCode(\"\")\n        setCodeSent(false)\n        toast.success(localization.phoneNumberUpdated)\n      }\n    }\n  )\n  const { mutate: updateUser, isPending: isRemoving } = useUpdateUser(\n    phoneClient,\n    {\n      onSuccess: () => {\n        setPhoneNumber(\"\")\n        toast.success(localization.phoneNumberRemoved)\n      }\n    }\n  )\n  const isPending = isSending || isVerifying || isRemoving\n\n  const handleSubmit = (event: SyntheticEvent<HTMLFormElement>) => {\n    event.preventDefault()\n    if (!codeSent) {\n      sendOtp({ phoneNumber })\n      return\n    }\n\n    verify({ phoneNumber, code, updatePhoneNumber: true })\n  }\n  const removePhoneNumber = () =>\n    updateUser({\n      phoneNumber: null\n    } as Parameters<PhoneNumberAuthClient[\"updateUser\"]>[0])\n\n  return (\n    <div>\n      <h2 className=\"mb-3 text-sm font-semibold\">\n        {localization.changePhoneNumber}\n      </h2>\n      <form onSubmit={handleSubmit}>\n        <Card className={cn(className)}>\n          <CardContent className=\"flex flex-col gap-6\">\n            {codeSent ? (\n              <>\n                <FieldDescription>\n                  {localization.codeSentTo.replace(\n                    \"{{phoneNumber}}\",\n                    phoneNumber\n                  )}\n                </FieldDescription>\n                <OtpField\n                  autoFocus\n                  disabled={isPending}\n                  label={localization.phoneCode}\n                  length={otpLength}\n                  name=\"otp\"\n                  value={code}\n                  onChange={setCode}\n                />\n              </>\n            ) : (\n              <Field data-invalid={Boolean(fieldError)}>\n                <FieldLabel htmlFor=\"settingsPhoneNumber\">\n                  {localization.phoneNumber}\n                </FieldLabel>\n                {session ? (\n                  <Input\n                    id=\"settingsPhoneNumber\"\n                    name=\"phoneNumber\"\n                    type=\"tel\"\n                    autoComplete=\"tel\"\n                    inputMode=\"tel\"\n                    value={phoneNumber}\n                    placeholder={localization.phoneNumberPlaceholder}\n                    required\n                    disabled={isPending}\n                    onChange={(event) => {\n                      setPhoneNumber(event.target.value)\n                      setFieldError(undefined)\n                    }}\n                    onInvalid={(event) => {\n                      event.preventDefault()\n                      setFieldError(event.currentTarget.validationMessage)\n                    }}\n                    aria-invalid={Boolean(fieldError)}\n                  />\n                ) : (\n                  <Skeleton>\n                    <Input className=\"invisible\" />\n                  </Skeleton>\n                )}\n                <FieldError>{fieldError}</FieldError>\n              </Field>\n            )}\n          </CardContent>\n          <CardFooter className=\"gap-3\">\n            {codeSent && (\n              <Button\n                type=\"button\"\n                size=\"sm\"\n                variant=\"outline\"\n                disabled={isPending}\n                onClick={() => {\n                  setCode(\"\")\n                  setCodeSent(false)\n                  setPhoneNumber(currentPhoneNumber)\n                }}\n              >\n                {localization.cancel}\n              </Button>\n            )}\n            <Button\n              type=\"submit\"\n              size=\"sm\"\n              disabled={\n                isPending || !session || (codeSent && code.length !== otpLength)\n              }\n            >\n              {(isSending || isVerifying) && <Spinner />}\n              {codeSent\n                ? localization.verifyCode\n                : localization.updatePhoneNumber}\n            </Button>\n            {!codeSent && currentPhoneNumber && (\n              <RemovePhoneNumberDialog\n                cancelLabel={localization.cancel}\n                description={localization.removePhoneNumberDescription}\n                isPending={isRemoving}\n                label={localization.removePhoneNumber}\n                title={localization.removePhoneNumberTitle}\n                onConfirm={removePhoneNumber}\n              />\n            )}\n          </CardFooter>\n        </Card>\n      </form>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/phone-number/change-phone-number.tsx"
    },
    {
      "path": "src/components/auth/phone-number/remove-phone-number-dialog.tsx",
      "content": "\"use client\"\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger\n} from \"@/components/ui/alert-dialog\"\nimport { Button } from \"@/components/ui/button\"\nimport { Spinner } from \"@/components/ui/spinner\"\n\nexport type RemovePhoneNumberDialogProps = {\n  description: string\n  isPending: boolean\n  label: string\n  title: string\n  cancelLabel: string\n  onConfirm: () => void\n}\n\n/** Confirm removal because it can disable a sign-in and recovery method. */\nexport function RemovePhoneNumberDialog({\n  cancelLabel,\n  description,\n  isPending,\n  label,\n  onConfirm,\n  title\n}: RemovePhoneNumberDialogProps) {\n  return (\n    <AlertDialog>\n      <AlertDialogTrigger asChild>\n        <Button type=\"button\" size=\"sm\" variant=\"outline\" disabled={isPending}>\n          {label}\n        </Button>\n      </AlertDialogTrigger>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          <AlertDialogDescription>{description}</AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel disabled={isPending}>\n            {cancelLabel}\n          </AlertDialogCancel>\n          <AlertDialogAction\n            variant=\"destructive\"\n            disabled={isPending}\n            onClick={onConfirm}\n          >\n            {isPending && <Spinner />}\n            {label}\n          </AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/phone-number/remove-phone-number-dialog.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"
}