{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "change-password",
  "title": "Change Password",
  "description": "A form component for users to change their password with current password verification.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "better-auth",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "card",
    "field",
    "input",
    "input-group",
    "skeleton",
    "sonner",
    "spinner",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/components/auth/settings/security/change-password.tsx",
      "content": "\"use client\"\n\nimport { getViewURL } from \"@better-auth-ui/core\"\nimport {\n  useAuth,\n  useChangePassword,\n  useFetchOptions,\n  useListAccounts,\n  useRequestPasswordReset,\n  useSession\n} from \"@better-auth-ui/react\"\nimport { Eye, EyeOff } from \"lucide-react\"\nimport { type SyntheticEvent, useState } from \"react\"\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent, CardFooter } from \"@/components/ui/card\"\nimport { Field, FieldError, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput\n} from \"@/components/ui/input-group\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { cn } from \"@/lib/utils\"\nimport { OpenEmailButton } from \"../../open-email-button\"\n\nexport type ChangePasswordProps = {\n  className?: string\n}\n\n/**\n * Render a card form for changing the authenticated user's password.\n *\n * When the user has a credential account, displays fields for current password,\n * new password, and optionally confirm password. When the user only has social\n * accounts, displays a prompt to set a password via the reset flow.\n *\n * @returns A JSX element containing the change-password or set-password card\n */\nexport function ChangePassword({ className }: ChangePasswordProps) {\n  const { authClient, emailAndPassword, localization } = useAuth()\n  const { data: session } = useSession(authClient)\n  const { data: accounts, isPending: isAccountsPending } =\n    useListAccounts(authClient)\n\n  const hasCredentialAccount = accounts?.some(\n    (account) => account.providerId === \"credential\"\n  )\n\n  if (!isAccountsPending && !hasCredentialAccount) {\n    return <SetPassword className={className} />\n  }\n\n  return (\n    <ChangePasswordForm\n      className={className}\n      emailAndPassword={emailAndPassword}\n      localization={localization}\n      session={isAccountsPending ? undefined : session}\n    />\n  )\n}\n\nfunction SetPassword({ className }: { className?: string }) {\n  const { authClient, basePaths, baseURL, localization, plugins, viewPaths } =\n    useAuth()\n  const { data: session } = useSession(authClient)\n  const { fetchOptions, resetFetchOptions } = useFetchOptions()\n  const [sentEmail, setSentEmail] = useState(\"\")\n\n  const { mutate: requestPasswordReset, isPending } = useRequestPasswordReset(\n    authClient,\n    {\n      onError: () => {\n        resetFetchOptions()\n      },\n      onSuccess: (_data, { email }) => {\n        setSentEmail(email)\n      }\n    }\n  )\n\n  const Captcha = plugins.find(\n    (plugin) => plugin.captchaComponent\n  )?.captchaComponent\n\n  const handleSetPassword = () => {\n    if (!session) return\n\n    requestPasswordReset({\n      email: session.user.email,\n      redirectTo: getViewURL(\n        baseURL,\n        basePaths.auth,\n        viewPaths.auth.resetPassword\n      ),\n      fetchOptions\n    })\n  }\n\n  return (\n    <div>\n      <h2 className=\"text-sm font-semibold mb-3\">\n        {localization.settings.changePassword}\n      </h2>\n\n      <Card className={cn(className)}>\n        <CardContent className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n          <div>\n            <p className=\"text-sm font-medium leading-tight\">\n              {localization.settings.setPassword}\n            </p>\n\n            <p className=\"text-muted-foreground text-xs mt-0.5\">\n              {localization.settings.setPasswordDescription}\n            </p>\n          </div>\n\n          {sentEmail ? (\n            <div className=\"flex flex-col gap-3 items-start sm:items-end\">\n              <p className=\"text-sm\" role=\"status\">\n                {localization.auth.resetLinkSentTo.replace(\n                  \"{{email}}\",\n                  sentEmail\n                )}\n              </p>\n\n              <OpenEmailButton email={sentEmail} className=\"w-auto\" />\n            </div>\n          ) : (\n            <div className=\"flex flex-col gap-3 items-start sm:items-end\">\n              {Captcha && <div>{Captcha}</div>}\n\n              <Button\n                size=\"sm\"\n                disabled={isPending || !session?.user.email}\n                onClick={handleSetPassword}\n              >\n                {isPending && <Spinner />}\n\n                {localization.auth.sendResetLink}\n              </Button>\n            </div>\n          )}\n        </CardContent>\n      </Card>\n    </div>\n  )\n}\n\nfunction ChangePasswordForm({\n  className,\n  emailAndPassword,\n  localization,\n  session\n}: {\n  className?: string\n  emailAndPassword: ReturnType<typeof useAuth>[\"emailAndPassword\"]\n  localization: ReturnType<typeof useAuth>[\"localization\"]\n  session: ReturnType<typeof useSession>[\"data\"]\n}) {\n  const { authClient } = useAuth()\n  const [currentPassword, setCurrentPassword] = useState(\"\")\n  const [newPassword, setNewPassword] = useState(\"\")\n  const [confirmPassword, setConfirmPassword] = useState(\"\")\n\n  const { mutate: changePassword, isPending } = useChangePassword(authClient, {\n    onError: () => {\n      setCurrentPassword(\"\")\n      setNewPassword(\"\")\n      setConfirmPassword(\"\")\n    },\n    onSuccess: () => {\n      setCurrentPassword(\"\")\n      setNewPassword(\"\")\n      setConfirmPassword(\"\")\n      toast.success(localization.settings.changePasswordSuccess)\n    }\n  })\n\n  const [isCurrentPasswordVisible, setIsCurrentPasswordVisible] =\n    useState(false)\n  const [isNewPasswordVisible, setIsNewPasswordVisible] = useState(false)\n  const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] =\n    useState(false)\n\n  const [fieldErrors, setFieldErrors] = useState<{\n    currentPassword?: string\n    newPassword?: string\n    confirmPassword?: string\n  }>({})\n\n  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    if (emailAndPassword.confirmPassword && newPassword !== confirmPassword) {\n      setCurrentPassword(\"\")\n      setNewPassword(\"\")\n      setConfirmPassword(\"\")\n      toast.error(localization.auth.passwordsDoNotMatch)\n      return\n    }\n\n    changePassword({\n      currentPassword,\n      newPassword,\n      revokeOtherSessions: true\n    })\n  }\n\n  return (\n    <div>\n      <h2 className=\"text-sm font-semibold mb-3\">\n        {localization.settings.changePassword}\n      </h2>\n\n      <form onSubmit={handleSubmit}>\n        <Card className={cn(className)}>\n          <CardContent className=\"flex flex-col gap-6\">\n            <Field data-invalid={!!fieldErrors.currentPassword}>\n              <FieldLabel htmlFor=\"currentPassword\">\n                {localization.settings.currentPassword}\n              </FieldLabel>\n\n              {session ? (\n                <InputGroup>\n                  <InputGroupInput\n                    id=\"currentPassword\"\n                    name=\"currentPassword\"\n                    type={isCurrentPasswordVisible ? \"text\" : \"password\"}\n                    autoComplete=\"current-password\"\n                    placeholder={\n                      localization.settings.currentPasswordPlaceholder\n                    }\n                    value={currentPassword}\n                    onChange={(e) => {\n                      setCurrentPassword(e.target.value)\n\n                      setFieldErrors((prev) => ({\n                        ...prev,\n                        currentPassword: undefined\n                      }))\n                    }}\n                    disabled={isPending}\n                    required\n                    onInvalid={(e) => {\n                      e.preventDefault()\n\n                      setFieldErrors((prev) => ({\n                        ...prev,\n                        currentPassword: (e.target as HTMLInputElement)\n                          .validationMessage\n                      }))\n                    }}\n                    aria-invalid={!!fieldErrors.currentPassword}\n                  />\n\n                  <InputGroupAddon align=\"inline-end\">\n                    <InputGroupButton\n                      size=\"icon-xs\"\n                      aria-label={\n                        isCurrentPasswordVisible\n                          ? localization.auth.hidePassword\n                          : localization.auth.showPassword\n                      }\n                      title={\n                        isCurrentPasswordVisible\n                          ? localization.auth.hidePassword\n                          : localization.auth.showPassword\n                      }\n                      onClick={() => {\n                        setIsCurrentPasswordVisible((visible) => !visible)\n                      }}\n                    >\n                      {isCurrentPasswordVisible ? <EyeOff /> : <Eye />}\n                    </InputGroupButton>\n                  </InputGroupAddon>\n                </InputGroup>\n              ) : (\n                <Skeleton>\n                  <Input className=\"invisible\" />\n                </Skeleton>\n              )}\n\n              <FieldError>{fieldErrors.currentPassword}</FieldError>\n            </Field>\n\n            <Field data-invalid={!!fieldErrors.newPassword}>\n              <FieldLabel htmlFor=\"newPassword\">\n                {localization.auth.newPassword}\n              </FieldLabel>\n\n              {session ? (\n                <InputGroup>\n                  <InputGroupInput\n                    id=\"newPassword\"\n                    name=\"newPassword\"\n                    type={isNewPasswordVisible ? \"text\" : \"password\"}\n                    autoComplete=\"new-password\"\n                    placeholder={localization.auth.newPasswordPlaceholder}\n                    value={newPassword}\n                    onChange={(e) => {\n                      setNewPassword(e.target.value)\n\n                      setFieldErrors((prev) => ({\n                        ...prev,\n                        newPassword: undefined\n                      }))\n                    }}\n                    minLength={emailAndPassword.minPasswordLength}\n                    maxLength={emailAndPassword.maxPasswordLength}\n                    disabled={isPending}\n                    required\n                    onInvalid={(e) => {\n                      e.preventDefault()\n                      setFieldErrors((prev) => ({\n                        ...prev,\n                        newPassword: (e.target as HTMLInputElement)\n                          .validationMessage\n                      }))\n                    }}\n                    aria-invalid={!!fieldErrors.newPassword}\n                  />\n\n                  <InputGroupAddon align=\"inline-end\">\n                    <InputGroupButton\n                      size=\"icon-xs\"\n                      aria-label={\n                        isNewPasswordVisible\n                          ? localization.auth.hidePassword\n                          : localization.auth.showPassword\n                      }\n                      onClick={() =>\n                        setIsNewPasswordVisible((visible) => !visible)\n                      }\n                    >\n                      {isNewPasswordVisible ? <EyeOff /> : <Eye />}\n                    </InputGroupButton>\n                  </InputGroupAddon>\n                </InputGroup>\n              ) : (\n                <Skeleton>\n                  <Input className=\"invisible\" />\n                </Skeleton>\n              )}\n\n              <FieldError>{fieldErrors.newPassword}</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                {session ? (\n                  <InputGroup>\n                    <InputGroupInput\n                      id=\"confirmPassword\"\n                      name=\"confirmPassword\"\n                      type={isConfirmPasswordVisible ? \"text\" : \"password\"}\n                      autoComplete=\"new-password\"\n                      placeholder={localization.auth.confirmPasswordPlaceholder}\n                      value={confirmPassword}\n                      onChange={(e) => {\n                        setConfirmPassword(e.target.value)\n\n                        setFieldErrors((prev) => ({\n                          ...prev,\n                          confirmPassword: undefined\n                        }))\n                      }}\n                      minLength={emailAndPassword.minPasswordLength}\n                      maxLength={emailAndPassword.maxPasswordLength}\n                      disabled={isPending}\n                      required\n                      onInvalid={(e) => {\n                        e.preventDefault()\n\n                        setFieldErrors((prev) => ({\n                          ...prev,\n                          confirmPassword: (e.target as HTMLInputElement)\n                            .validationMessage\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                        onClick={() =>\n                          setIsConfirmPasswordVisible((visible) => !visible)\n                        }\n                      >\n                        {isConfirmPasswordVisible ? <EyeOff /> : <Eye />}\n                      </InputGroupButton>\n                    </InputGroupAddon>\n                  </InputGroup>\n                ) : (\n                  <Skeleton>\n                    <Input className=\"invisible\" />\n                  </Skeleton>\n                )}\n\n                <FieldError>{fieldErrors.confirmPassword}</FieldError>\n              </Field>\n            )}\n          </CardContent>\n\n          <CardFooter>\n            <Button type=\"submit\" size=\"sm\" disabled={isPending || !session}>\n              {isPending && <Spinner />}\n\n              {localization.settings.updatePassword}\n            </Button>\n          </CardFooter>\n        </Card>\n      </form>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/settings/security/change-password.tsx"
    },
    {
      "path": "src/components/auth/open-email-button.tsx",
      "content": "\"use client\"\n\nimport { createQrCodeSvgData, getEmailProviderLink } from \"@better-auth-ui/core\"\nimport { useAuth } from \"@better-auth-ui/react\"\nimport type { VariantProps } from \"class-variance-authority\"\nimport { QrCode } from \"lucide-react\"\nimport { useMemo } from \"react\"\n\nimport { buttonVariants } from \"@/components/ui/button\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger\n} from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\n\nexport type OpenEmailButtonProps = {\n  /** Email address used to detect the provider, e.g. from the verify-email flow. */\n  email: string\n  className?: string\n  /**\n   * Button variant. Defaults to the primary style for dead-end views where\n   * opening the inbox is the only action; pass `\"secondary\"` where it sits\n   * beside a submit button that should stay the primary call to action.\n   */\n  variant?: VariantProps<typeof buttonVariants>[\"variant\"]\n}\n\n/**\n * Render a button that opens the user's email provider login page in a new\n * tab. Hovering or focusing the button reveals a QR code for opening the same\n * provider on another device.\n *\n * The provider is resolved from the email domain via the curated\n * `@mikkelscheike/email-provider-links` dataset (Gmail, Outlook, GMX, etc.).\n * Renders nothing when the domain is empty or not a known provider.\n *\n * @param email - Email address to resolve the provider from.\n * @param className - Additional CSS classes applied to the button.\n * @param variant - Button variant. Defaults to the primary style.\n * @returns The open-email button, or `null` when no provider matches.\n */\nexport function OpenEmailButton({\n  email,\n  className,\n  variant\n}: OpenEmailButtonProps) {\n  const { localization } = useAuth()\n\n  const provider = getEmailProviderLink(email)\n  const loginUrl = provider?.loginUrl\n  const qrCode = useMemo(\n    () => (loginUrl ? createQrCodeSvgData(loginUrl) : null),\n    [loginUrl]\n  )\n\n  if (!provider || !qrCode) return null\n\n  const scanLabel = localization.auth.scanToOpenEmailProvider.replace(\n    \"{{provider}}\",\n    provider.companyProvider\n  )\n\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger\n          type=\"button\"\n          className={cn(buttonVariants({ variant }), \"w-full\", className)}\n          onClick={() =>\n            window.open(provider.loginUrl, \"_blank\", \"noopener,noreferrer\")\n          }\n        >\n          {localization.auth.openEmailProvider.replace(\n            \"{{provider}}\",\n            provider.companyProvider\n          )}\n          <QrCode data-icon=\"inline-end\" />\n        </TooltipTrigger>\n        <TooltipContent\n          sideOffset={8}\n          className=\"flex-col items-center gap-2 p-3\"\n        >\n          <svg\n            viewBox={`0 0 ${qrCode.size} ${qrCode.size}`}\n            aria-hidden=\"true\"\n            focusable=\"false\"\n            className=\"size-40\"\n          >\n            <path fill=\"white\" d={`M0 0h${qrCode.size}v${qrCode.size}H0z`} />\n            <path fill=\"black\" d={qrCode.path} shapeRendering=\"crispEdges\" />\n          </svg>\n          <p className=\"max-w-40 text-center leading-snug\">{scanLabel}</p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/open-email-button.tsx"
    }
  ],
  "type": "registry:component"
}