{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "user-profile",
  "title": "User Profile",
  "description": "A form component for editing user profile information including name and avatar.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "@tanstack/react-pacer",
    "better-auth",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "card",
    "dropdown-menu",
    "field",
    "input",
    "input-group",
    "skeleton",
    "sonner",
    "spinner",
    "https://better-auth-ui.com/r/radix-nova/additional-field.json",
    "https://better-auth-ui.com/r/radix-nova/user-avatar.json"
  ],
  "files": [
    {
      "path": "src/components/auth/settings/account/user-profile.tsx",
      "content": "\"use client\"\n\nimport {\n  type AdditionalFieldValue,\n  parseAdditionalFieldValue\n} from \"@better-auth-ui/core\"\nimport {\n  type UsernameAuthClient,\n  useAuth,\n  useSession,\n  useUpdateUser\n} from \"@better-auth-ui/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 { Skeleton } from \"@/components/ui/skeleton\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { cn } from \"@/lib/utils\"\nimport { AdditionalField } from \"../../additional-field\"\nimport { ChangeAvatar } from \"./change-avatar\"\n\nexport type UserProfileProps = {\n  className?: string\n}\n\n/**\n * Render a profile card that lets the authenticated user view and update their display name, username, and avatar.\n *\n * @param className - Optional additional CSS class names applied to the card container\n * @returns A JSX element containing the profile card with avatar upload and editable name/username fields\n */\nexport function UserProfile({ className }: UserProfileProps) {\n  const { additionalFields, authClient, localization } = useAuth()\n  const { data: session } = useSession(authClient as UsernameAuthClient)\n\n  const { mutate: updateUser, isPending } = useUpdateUser(authClient, {\n    onSuccess: () => toast.success(localization.settings.profileUpdatedSuccess)\n  })\n\n  const [fieldErrors, setFieldErrors] = useState<{\n    name?: string\n  }>({})\n\n  async function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {\n    e.preventDefault()\n\n    const formData = new FormData(e.currentTarget)\n    const name = formData.get(\"name\") as string\n\n    const additionalFieldValues: Record<string, unknown> = {}\n\n    for (const field of additionalFields ?? []) {\n      if (field.profile === false || 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      // `null` = explicit clear (forward to backend); `undefined` = omitted.\n      if (value !== undefined) {\n        additionalFieldValues[field.name] = value\n      }\n    }\n\n    updateUser({\n      name,\n      ...additionalFieldValues\n    })\n  }\n\n  return (\n    <div>\n      <h2 className=\"text-sm font-semibold mb-3\">\n        {localization.settings.userProfile}\n      </h2>\n\n      <form onSubmit={handleSubmit}>\n        <Card className={cn(className)}>\n          <CardContent className=\"flex flex-col gap-6\">\n            <ChangeAvatar />\n\n            <Field data-invalid={!!fieldErrors.name}>\n              <FieldLabel htmlFor=\"name\">{localization.auth.name}</FieldLabel>\n\n              {session ? (\n                <Input\n                  key={session?.user.name}\n                  id=\"name\"\n                  name=\"name\"\n                  autoComplete=\"name\"\n                  defaultValue={session?.user.name}\n                  placeholder={localization.auth.name}\n                  disabled={isPending}\n                  required\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: (e.target as HTMLInputElement).validationMessage\n                    }))\n                  }}\n                  aria-invalid={!!fieldErrors.name}\n                />\n              ) : (\n                <Skeleton>\n                  <Input className=\"invisible\" />\n                </Skeleton>\n              )}\n\n              <FieldError>{fieldErrors.name}</FieldError>\n            </Field>\n\n            {additionalFields?.map((field) => {\n              if (field.profile === false) return null\n\n              if (!session) {\n                if (field.inputType === \"hidden\") {\n                  return null\n                }\n\n                return (\n                  <Skeleton key={field.name}>\n                    <Input className=\"invisible\" />\n                  </Skeleton>\n                )\n              }\n\n              const value = (session.user as Record<string, unknown>)[\n                field.name\n              ]\n\n              // Re-mount when the session value loads so the field's\n              // uncontrolled `defaultValue` reflects the latest data.\n              const key = `${field.name}:${\n                value instanceof Date\n                  ? value.toISOString()\n                  : String(value ?? \"\")\n              }`\n\n              return (\n                <AdditionalField\n                  key={key}\n                  name={field.name}\n                  field={{\n                    ...field,\n                    // `defaultValue` is sign-up-only; on the profile we\n                    // always seed from the session.\n                    defaultValue: value as AdditionalFieldValue | null\n                  }}\n                  isPending={isPending}\n                />\n              )\n            })}\n          </CardContent>\n\n          <CardFooter>\n            <Button type=\"submit\" size=\"sm\" disabled={isPending || !session}>\n              {isPending && <Spinner />}\n\n              {localization.settings.saveChanges}\n            </Button>\n          </CardFooter>\n        </Card>\n      </form>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/settings/account/user-profile.tsx"
    },
    {
      "path": "src/components/auth/settings/account/change-avatar.tsx",
      "content": "\"use client\"\n\nimport { fileToAvatarDataUrl } from \"@better-auth-ui/core\"\nimport { useAuth, useSession, useUpdateUser } from \"@better-auth-ui/react\"\nimport { Trash2, Upload } from \"lucide-react\"\nimport { type ChangeEvent, useRef, useState } from \"react\"\nimport { toast } from \"sonner\"\nimport { UserAvatar } from \"@/components/auth/user/user-avatar\"\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger\n} from \"@/components/ui/dropdown-menu\"\nimport { Field, FieldLabel } from \"@/components/ui/field\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { cn } from \"@/lib/utils\"\n\nexport type ChangeAvatarProps = {\n  className?: string\n}\n\nexport function ChangeAvatar({ className }: ChangeAvatarProps) {\n  const { authClient, localization, avatar } = useAuth()\n  const { data: session } = useSession(authClient)\n\n  const { mutate: updateUser, isPending: updatePending } =\n    useUpdateUser(authClient)\n\n  const fileInputRef = useRef<HTMLInputElement>(null)\n  const [isUploading, setIsUploading] = useState(false)\n  const [isDeleting, setIsDeleting] = useState(false)\n\n  const isPending = updatePending || isUploading || isDeleting\n\n  async function handleFileChange(e: ChangeEvent<HTMLInputElement>) {\n    const file = e.target.files?.[0]\n    if (!file) return\n\n    e.target.value = \"\"\n\n    setIsUploading(true)\n\n    try {\n      const resized =\n        (await avatar.resize?.(file, avatar.size, avatar.extension)) || file\n\n      const image =\n        (await avatar.upload?.(resized)) || (await fileToAvatarDataUrl(resized))\n\n      updateUser(\n        { image },\n        {\n          onSuccess: () =>\n            toast.success(localization.settings.avatarChangedSuccess)\n        }\n      )\n    } catch (error) {\n      if (error instanceof Error) {\n        toast.error(error.message)\n      }\n    }\n\n    setIsUploading(false)\n  }\n\n  async function handleDelete() {\n    const currentImage = session?.user.image\n\n    updateUser(\n      { image: null },\n      {\n        onSuccess: async () => {\n          if (currentImage) {\n            setIsDeleting(true)\n            try {\n              await avatar.delete?.(currentImage)\n            } finally {\n              setIsDeleting(false)\n            }\n          }\n\n          toast.success(localization.settings.avatarDeletedSuccess)\n        }\n      }\n    )\n  }\n\n  return (\n    <Field className={className}>\n      <FieldLabel>{localization.settings.avatar}</FieldLabel>\n\n      <input\n        ref={fileInputRef}\n        type=\"file\"\n        accept=\"image/*\"\n        className=\"hidden\"\n        onChange={handleFileChange}\n      />\n\n      <div className=\"flex items-center gap-4\">\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          className=\"p-0 h-auto w-auto rounded-full\"\n          disabled={isPending}\n          onClick={() => fileInputRef.current?.click()}\n        >\n          <UserAvatar className=\"size-12\" isPending={isPending} />\n        </Button>\n\n        <DropdownMenu>\n          <DropdownMenuTrigger\n            className={cn(buttonVariants({ variant: \"secondary\", size: \"sm\" }))}\n            disabled={!session || isPending}\n          >\n            {isPending && <Spinner />}\n\n            {localization.settings.changeAvatar}\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent className=\"min-w-fit\">\n            <DropdownMenuItem onClick={() => fileInputRef.current?.click()}>\n              <Upload className=\"text-muted-foreground\" />\n\n              {localization.settings.uploadAvatar}\n            </DropdownMenuItem>\n\n            <DropdownMenuItem\n              variant=\"destructive\"\n              disabled={!session?.user.image}\n              onClick={handleDelete}\n            >\n              <Trash2 />\n\n              {localization.settings.deleteAvatar}\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    </Field>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/settings/account/change-avatar.tsx"
    }
  ],
  "type": "registry:component"
}