# Compact Profile Card

> Compact centered settings card with avatar upload, name and email fields, timezone, density radios, notification checkboxes, a two-factor switch, and delete-account confirmation.

A shadcn/ui block from the [@7ovr](https://7ovr.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/7ovr/settings-2).

- Page, with a live preview: https://shadcn.exchange/r/7ovr/settings-2
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/7ovr/settings-2-light-ec95f0a7e8014e90.png
- Registry item: https://7ovr.com/r/settings-2.json
- Categories: settings
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://7ovr.com/r/settings-2.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@base-ui/react`
- Registry items: `alert-dialog`, `avatar`, `button`, `card`, `checkbox`, `field`, `input`, `radio-group`, `select`, `separator`, `sonner`, `switch`

## Notes from the registry

Requires a `base-*` style in components.json (the `shadcn init` default). Legacy `new-york`/`radix-*` styles install Radix primitives, which reject the `render` prop this block uses.

## Files

### `registry/blocks/settings/2/settings-block.tsx`

```tsx
"use client"

import * as React from "react"
import { toast } from "sonner"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,
  FieldDescription,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { Switch } from "@/components/ui/switch"
import { Toaster } from "@/components/ui/sonner"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

const profile = {
  name: "Marcus Trent",
  email: "marcus.trent@acme.com",
  role: "Product Designer",
  initials: "MT",
  avatar: "https://i.pravatar.cc/150?img=13",
  twoFactor: true,
}

const TIMEZONES = [
  { value: "pst", label: "Pacific (UTC−8)" },
  { value: "est", label: "Eastern (UTC−5)" },
  { value: "gmt", label: "London (UTC+0)" },
  { value: "cet", label: "Central Europe (UTC+1)" },
  { value: "jst", label: "Tokyo (UTC+9)" },
]

const DENSITIES = [
  { value: "comfortable", label: "Comfortable" },
  { value: "compact", label: "Compact" },
]

const NOTIFICATION_PREFS = [
  { id: "email-digest", label: "Weekly email digest" },
  { id: "mentions", label: "Mentions and replies" },
  { id: "product-news", label: "Product news" },
]

export default function SettingsBlock() {
  const [name, setName] = React.useState(profile.name)
  const [email, setEmail] = React.useState(profile.email)
  const [role, setRole] = React.useState(profile.role)
  const [twoFactor, setTwoFactor] = React.useState(profile.twoFactor)
  const [timezone, setTimezone] = React.useState("pst")
  const [density, setDensity] = React.useState("comfortable")
  const [prefs, setPrefs] = React.useState<Record<string, boolean>>({
    "email-digest": true,
    mentions: true,
    "product-news": false,
  })
  const [deleteOpen, setDeleteOpen] = React.useState(false)
  const avatarInputRef = React.useRef<HTMLInputElement>(null)

  function handleAvatarChange(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0]
    if (file) {
      toast.success("Photo updated", {
        id: "photo-updated",
        description: file.name,
      })
    }
  }

  function handleSave() {
    toast.success("Profile updated", {
      id: "profile-updated",
      description: "Your changes have been saved.",
    })
  }

  function handleDelete() {
    setDeleteOpen(false)
    toast.error("Account scheduled for deletion", {
      id: "account-deletion",
      description: "You can undo this within 30 days from your inbox.",
    })
  }

  return (
    <section className="flex w-full items-center justify-center bg-background px-6 py-12 text-foreground">
      <Card className="w-full max-w-sm border-border shadow-sm">
        <CardHeader className="pb-4">
          <CardTitle className="text-base font-semibold tracking-tight">
            Profile
          </CardTitle>
          <CardDescription className="text-xs leading-relaxed">
            Update your personal details and preferences.
          </CardDescription>
        </CardHeader>

        <CardContent className="flex flex-col gap-0 p-0">
          <div className="flex items-center gap-4 border-y border-border bg-muted/40 px-6 py-4">
            <div className="relative shrink-0">
              <Avatar className="size-14 border border-border">
                <AvatarImage
                  src={profile.avatar}
                  alt={profile.name}
                  className="grayscale"
                />
                <AvatarFallback className="bg-muted text-sm font-medium text-foreground">
                  {profile.initials}
                </AvatarFallback>
              </Avatar>
              <span className="absolute right-0.5 bottom-0.5 size-2.5 border-2 border-card bg-primary" />
            </div>

            <div className="flex min-w-0 flex-1 flex-col gap-0.5">
              <span className="truncate text-sm leading-tight font-semibold">
                {name}
              </span>
              <span className="truncate text-xs text-muted-foreground">
                {role}
              </span>
            </div>

            <input
              ref={avatarInputRef}
              type="file"
              accept="image/*"
              className="hidden"
              aria-hidden="true"
              tabIndex={-1}
              onChange={handleAvatarChange}
            />
            <Button
              variant="outline"
              size="xs"
              className="shrink-0 gap-1.5 text-xs"
              onClick={() => avatarInputRef.current?.click()}
            >
              <IconPlaceholder
                lucide="Upload"
                tabler="IconUpload"
                hugeicons="UploadIcon"
                phosphor="Upload"
                remixicon="RiUploadLine"
                className="size-3.5"
              />
              Upload
            </Button>
          </div>

          <div className="px-6 py-5">
            <FieldGroup className="flex flex-col gap-4">
              <Field>
                <FieldLabel
                  htmlFor="full-name"
                  className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground"
                >
                  <IconPlaceholder
                    lucide="User"
                    tabler="IconUser"
                    hugeicons="UserIcon"
                    phosphor="User"
                    remixicon="RiUserLine"
                    className="size-3.5"
                  />
                  Full name
                </FieldLabel>
                <Input
                  id="full-name"
                  type="text"
                  value={name}
                  onChange={(e) => setName(e.target.value)}
                  placeholder="Your full name"
                  className="mt-1.5 h-8 text-sm transition-shadow focus-visible:shadow-sm"
                />
              </Field>

              <Field>
                <FieldLabel
                  htmlFor="email-address"
                  className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground"
                >
                  <IconPlaceholder
                    lucide="Mail"
                    tabler="IconMail"
                    hugeicons="MailIcon"
                    phosphor="Envelope"
                    remixicon="RiMailLine"
                    className="size-3.5"
                  />
                  Email address
                </FieldLabel>
                <Input
                  id="email-address"
                  type="email"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  placeholder="you@acme.com"
                  className="mt-1.5 h-8 text-sm transition-shadow focus-visible:shadow-sm"
                />
                <FieldDescription className="mt-1.5 text-[11px] leading-relaxed text-muted-foreground">
                  Changing your email will require re-verification.
                </FieldDescription>
              </Field>

              <Field>
                <FieldLabel
                  htmlFor="job-title"
                  className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground"
                >
                  <IconPlaceholder
                    lucide="Briefcase"
                    tabler="IconBriefcase"
                    hugeicons="BriefcaseIcon"
                    phosphor="Briefcase"
                    remixicon="RiBriefcaseLine"
                    className="size-3.5"
                  />
                  Job title
                </FieldLabel>
                <Input
                  id="job-title"
                  type="text"
                  value={role}
                  onChange={(e) => setRole(e.target.value)}
                  placeholder="e.g. Senior Engineer"
                  className="mt-1.5 h-8 text-sm transition-shadow focus-visible:shadow-sm"
                />
              </Field>

              <Field>
                <FieldLabel
                  htmlFor="timezone"
                  className="text-xs font-medium text-muted-foreground"
                >
                  Timezone
                </FieldLabel>
                <Select
                  items={TIMEZONES}
                  value={timezone}
                  onValueChange={(v) => v && setTimezone(v)}
                >
                  <SelectTrigger
                    id="timezone"
                    className="mt-1.5 h-8 w-full text-sm"
                  >
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent alignItemWithTrigger={false}>
                    {TIMEZONES.map((tz) => (
                      <SelectItem key={tz.value} value={tz.value}>
                        {tz.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </Field>
            </FieldGroup>
          </div>

          <Separator />

          <div className="flex flex-col gap-5 px-6 py-5">
            <FieldSet>
              <FieldLegend variant="label">Interface density</FieldLegend>
              <RadioGroup
                value={density}
                onValueChange={(value) => setDensity(String(value))}
              >
                {DENSITIES.map((option) => (
                  <Field
                    key={option.value}
                    orientation="horizontal"
                    className="gap-2"
                  >
                    <RadioGroupItem
                      value={option.value}
                      id={`density-${option.value}`}
                    />
                    <FieldLabel
                      htmlFor={`density-${option.value}`}
                      className="text-xs font-normal"
                    >
                      {option.label}
                    </FieldLabel>
                  </Field>
                ))}
              </RadioGroup>
            </FieldSet>

            <FieldSet>
              <FieldLegend variant="label">Email notifications</FieldLegend>
              <div data-slot="checkbox-group" className="flex flex-col gap-3">
                {NOTIFICATION_PREFS.map((pref) => (
                  <Field
                    key={pref.id}
                    orientation="horizontal"
                    className="gap-2"
                  >
                    <Checkbox
                      id={pref.id}
                      checked={prefs[pref.id]}
                      onCheckedChange={(checked) =>
                        setPrefs((prev) => ({
                          ...prev,
                          [pref.id]: checked === true,
                        }))
                      }
                    />
                    <FieldLabel
                      htmlFor={pref.id}
                      className="text-xs font-normal"
                    >
                      {pref.label}
                    </FieldLabel>
                  </Field>
                ))}
              </div>
            </FieldSet>
          </div>

          <Separator />

          <div className="px-6 py-4">
            <div className="flex items-start justify-between gap-4">
              <div className="flex flex-col gap-1">
                <div className="flex items-center gap-1.5">
                  <IconPlaceholder
                    lucide="ShieldCheck"
                    tabler="IconShieldCheck"
                    hugeicons="Shield01Icon"
                    phosphor="ShieldCheck"
                    remixicon="RiShieldCheckLine"
                    className="size-3.5 text-muted-foreground"
                  />
                  <span className="text-xs font-medium">
                    Two-factor authentication
                  </span>
                </div>
                <p className="text-[11px] leading-relaxed text-muted-foreground">
                  Require a verification code on every sign-in.
                </p>
              </div>
              <Switch
                id="two-factor"
                checked={twoFactor}
                onCheckedChange={setTwoFactor}
                className="mt-0.5 shrink-0"
              />
            </div>
          </div>
        </CardContent>

        <CardFooter className="flex items-center justify-between gap-3 px-6 py-3">
          <AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
            <AlertDialogTrigger
              render={
                <Button
                  variant="ghost"
                  size="sm"
                  className="text-xs text-destructive hover:text-destructive"
                >
                  Delete Account
                </Button>
              }
            />
            <AlertDialogContent>
              <AlertDialogHeader>
                <AlertDialogTitle>Delete your account?</AlertDialogTitle>
                <AlertDialogDescription>
                  This permanently removes your profile and all associated data.
                  This action cannot be undone.
                </AlertDialogDescription>
              </AlertDialogHeader>
              <AlertDialogFooter>
                <AlertDialogCancel>Cancel</AlertDialogCancel>
                <AlertDialogAction variant="destructive" onClick={handleDelete}>
                  Delete Account
                </AlertDialogAction>
              </AlertDialogFooter>
            </AlertDialogContent>
          </AlertDialog>
          <Button size="sm" className="gap-1.5 text-xs" onClick={handleSave}>
            Save Changes
          </Button>
        </CardFooter>
      </Card>
      <Toaster />
    </section>
  )
}
```
