{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sign-up",
  "title": "Sign Up",
  "description": "A complete sign-up form component with name, email, password fields, and social provider support.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "@tanstack/react-pacer",
    "better-auth",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "input",
    "field",
    "input-group",
    "sonner",
    "spinner",
    "https://better-auth-ui.com/r/radix-nova/additional-field.json"
  ],
  "files": [
    {
      "path": "src/components/auth/sign-up.tsx",
      "content": "\"use client\"\n\nimport {\n  authMutationKeys,\n  getAuthLinkURL,\n  parseAdditionalFieldValue\n} from \"@better-auth-ui/core\"\nimport {\n  AuthPrompts,\n  useAuth,\n  useFetchOptions,\n  useSignUpEmail\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 { toast } from \"sonner\"\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  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 { cn } from \"@/lib/utils\"\nimport { AdditionalField } from \"./additional-field\"\nimport { ProviderButtons, type SocialLayout } from \"./provider-buttons\"\n\nexport type SignUpProps = {\n  className?: string\n  socialLayout?: SocialLayout\n  socialPosition?: \"top\" | \"bottom\"\n  /**\n   * Runs instead of the post-sign-up redirect, but only when the sign-up\n   * created an immediately usable session. Email verification still takes\n   * priority, and social sign-ups are unaffected.\n   */\n  onSignUpSuccess?: () => void\n}\n\n/**\n * Renders a sign-up form with name, email, and password fields, optional social provider buttons, and submission handling.\n *\n * Submits credentials to the configured auth client and handles the response:\n * - If email verification is required, shows a notification and navigates to sign-in\n * - On success, refreshes the session and navigates to the configured redirect path\n * - On failure, displays error toasts\n * - Manages a pending state while the request is in-flight\n *\n * @param className - Additional CSS classes applied to the outer container\n * @param socialLayout - Social layout to apply to the component\n * @param socialPosition - Social position to apply to the component\n * @param onSignUpSuccess - Replaces the post-sign-up redirect when the new account is immediately usable\n * @returns The sign-up form React element.\n */\nexport function SignUp({\n  className,\n  socialLayout,\n  socialPosition = \"bottom\",\n  onSignUpSuccess\n}: SignUpProps) {\n  const {\n    additionalFields,\n    authClient,\n    basePaths,\n    emailAndPassword,\n    localization,\n    plugins,\n    redirectTo,\n    socialProviders,\n    viewPaths,\n    navigate,\n    Link\n  } = useAuth()\n\n  const { fetchOptions, resetFetchOptions } = useFetchOptions()\n\n  const [password, setPassword] = useState(\"\")\n  const [confirmPassword, setConfirmPassword] = useState(\"\")\n\n  const { mutate: signUpEmail, isPending: signUpEmailPending } = useSignUpEmail(\n    authClient,\n    {\n      onError: () => {\n        setPassword(\"\")\n        setConfirmPassword(\"\")\n        resetFetchOptions()\n      },\n      onSuccess: (_data, { email }) => {\n        if (emailAndPassword?.requireEmailVerification) {\n          sessionStorage.setItem(\"better-auth-ui.verify-email\", email)\n          navigate({\n            to: getAuthLinkURL(\n              `${basePaths.auth}/${viewPaths.auth.verifyEmail}`,\n              redirectTo\n            )\n          })\n        } else if (onSignUpSuccess) {\n          onSignUpSuccess()\n        } else {\n          navigate({ to: redirectTo })\n        }\n      }\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\n  const Captcha = plugins.find(\n    (plugin) => plugin.captchaComponent\n  )?.captchaComponent\n\n  const [isPasswordVisible, setIsPasswordVisible] = useState(false)\n  const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] =\n    useState(false)\n\n  const [fieldErrors, setFieldErrors] = useState<{\n    name?: string\n    email?: string\n    password?: string\n    confirmPassword?: string\n  }>({})\n\n  const handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    const formData = new FormData(e.currentTarget)\n    // `emailAndPassword.name === false` hides the name field and submits \"\".\n    const name = (formData.get(\"name\") as string | null) ?? \"\"\n    const email = formData.get(\"email\") as string\n\n    if (emailAndPassword?.confirmPassword && password !== confirmPassword) {\n      toast.error(localization.auth.passwordsDoNotMatch)\n      setPassword(\"\")\n      setConfirmPassword(\"\")\n      return\n    }\n\n    const additionalFieldValues: Record<string, unknown> = {}\n\n    for (const field of additionalFields ?? []) {\n      if (!field.signUp || field.readOnly) continue\n      const value = parseAdditionalFieldValue(\n        field,\n        formData.get(field.name) as string | null\n      )\n\n      if (field.validate) {\n        try {\n          await field.validate(value)\n        } catch (error) {\n          toast.error(error instanceof Error ? error.message : String(error))\n          return\n        }\n      }\n\n      if (value !== undefined) {\n        additionalFieldValues[field.name] = value\n      }\n    }\n\n    signUpEmail({\n      name,\n      email,\n      password,\n      ...additionalFieldValues,\n      fetchOptions\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=\"signUp\" />\n      <CardHeader>\n        <CardTitle className=\"text-xl font-semibold\">\n          {localization.auth.signUp}\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=\"signUp\" />\n              )}\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          )}\n\n          {emailAndPassword?.enabled && (\n            <form onSubmit={handleSubmit}>\n              <FieldGroup>\n                {emailAndPassword.name !== false && (\n                  <Field data-invalid={!!fieldErrors.name}>\n                    <FieldLabel htmlFor=\"name\">\n                      {localization.auth.name}\n                    </FieldLabel>\n\n                    <Input\n                      id=\"name\"\n                      name=\"name\"\n                      type=\"text\"\n                      autoComplete=\"name\"\n                      placeholder={localization.auth.namePlaceholder}\n                      required\n                      disabled={isPending}\n                      onChange={() => {\n                        setFieldErrors((prev) => ({\n                          ...prev,\n                          name: undefined\n                        }))\n                      }}\n                      onInvalid={(e) => {\n                        e.preventDefault()\n\n                        setFieldErrors((prev) => ({\n                          ...prev,\n                          name: localization.auth.fieldRequired\n                        }))\n                      }}\n                      aria-invalid={!!fieldErrors.name}\n                    />\n\n                    <FieldError>{fieldErrors.name}</FieldError>\n                  </Field>\n                )}\n\n                <Field data-invalid={!!fieldErrors.email}>\n                  <FieldLabel htmlFor=\"email\">\n                    {localization.auth.email}\n                  </FieldLabel>\n\n                  <Input\n                    id=\"email\"\n                    name=\"email\"\n                    type=\"email\"\n                    autoComplete=\"email\"\n                    placeholder={localization.auth.emailPlaceholder}\n                    required\n                    disabled={isPending}\n                    onChange={() => {\n                      setFieldErrors((prev) => ({\n                        ...prev,\n                        email: undefined\n                      }))\n                    }}\n                    onInvalid={(e) => {\n                      e.preventDefault()\n                      const el = e.target as HTMLInputElement\n                      const msg = el.validity.valueMissing\n                        ? localization.auth.fieldRequired\n                        : localization.auth.invalidEmail\n\n                      setFieldErrors((prev) => ({\n                        ...prev,\n                        email: msg\n                      }))\n                    }}\n                    aria-invalid={!!fieldErrors.email}\n                  />\n\n                  <FieldError>{fieldErrors.email}</FieldError>\n                </Field>\n\n                {additionalFields?.map(\n                  (field) =>\n                    field.signUp === \"above\" && (\n                      <AdditionalField\n                        key={field.name}\n                        name={field.name}\n                        field={field}\n                        isPending={isPending}\n                        optionalLabel={localization.auth.optional}\n                      />\n                    )\n                )}\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=\"new-password\"\n                      value={password}\n                      onChange={(e) => {\n                        setPassword(e.target.value)\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?.confirmPassword && (\n                  <Field data-invalid={!!fieldErrors.confirmPassword}>\n                    <FieldLabel htmlFor=\"confirmPassword\">\n                      {localization.auth.confirmPassword}\n                    </FieldLabel>\n\n                    <InputGroup>\n                      <InputGroupInput\n                        id=\"confirmPassword\"\n                        name=\"confirmPassword\"\n                        type={isConfirmPasswordVisible ? \"text\" : \"password\"}\n                        autoComplete=\"new-password\"\n                        value={confirmPassword}\n                        onChange={(e) => {\n                          setConfirmPassword(e.target.value)\n\n                          setFieldErrors((prev) => ({\n                            ...prev,\n                            confirmPassword: undefined\n                          }))\n                        }}\n                        placeholder={\n                          localization.auth.confirmPasswordPlaceholder\n                        }\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                            confirmPassword: msg\n                          }))\n                        }}\n                        aria-invalid={!!fieldErrors.confirmPassword}\n                      />\n\n                      <InputGroupAddon align=\"inline-end\">\n                        <InputGroupButton\n                          size=\"icon-xs\"\n                          aria-label={\n                            isConfirmPasswordVisible\n                              ? localization.auth.hidePassword\n                              : localization.auth.showPassword\n                          }\n                          title={\n                            isConfirmPasswordVisible\n                              ? localization.auth.hidePassword\n                              : localization.auth.showPassword\n                          }\n                          onClick={() =>\n                            setIsConfirmPasswordVisible((visible) => !visible)\n                          }\n                        >\n                          {isConfirmPasswordVisible ? <EyeOff /> : <Eye />}\n                        </InputGroupButton>\n                      </InputGroupAddon>\n                    </InputGroup>\n\n                    <FieldError>{fieldErrors.confirmPassword}</FieldError>\n                  </Field>\n                )}\n\n                {additionalFields?.map(\n                  (field) =>\n                    field.signUp &&\n                    field.signUp !== \"above\" && (\n                      <AdditionalField\n                        key={field.name}\n                        name={field.name}\n                        field={field}\n                        isPending={isPending}\n                        optionalLabel={localization.auth.optional}\n                      />\n                    )\n                )}\n\n                {Captcha && (\n                  <div className=\"flex justify-center\">{Captcha}</div>\n                )}\n\n                <div className=\"flex flex-col gap-3\">\n                  <Button type=\"submit\" disabled={isPending}>\n                    {signUpEmailPending && <Spinner />}\n\n                    {localization.auth.signUp}\n                  </Button>\n\n                  {plugins.flatMap((plugin) =>\n                    (plugin.authButtons ?? []).map((AuthButton, index) => (\n                      <AuthButton\n                        key={`${plugin.id}-${index.toString()}`}\n                        view=\"signUp\"\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=\"signUp\" />\n              )}\n            </>\n          )}\n        </div>\n\n        {emailAndPassword?.enabled && (\n          <div className=\"flex flex-col gap-3 items-center w-full mt-4\">\n            <FieldDescription className=\"text-center\">\n              {localization.auth.alreadyHaveAnAccount}{\" \"}\n              <Link\n                href={getAuthLinkURL(\n                  `${basePaths.auth}/${viewPaths.auth.signIn}`,\n                  redirectTo\n                )}\n                className=\"underline underline-offset-4\"\n              >\n                {localization.auth.signIn}\n              </Link>\n            </FieldDescription>\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/sign-up.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"
    }
  ],
  "type": "registry:component"
}