{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "settings-1",
  "title": "Settings 1: profile and account",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An account settings page: profile with username availability, sign-in details with a change-password dialog, language, time zone and appearance, a type-to-confirm delete, and a sticky save bar that appears only when something changed.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/alert-dialog.json",
    "https://ui.ballmac.com/r/avatar.json",
    "https://ui.ballmac.com/r/badge.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/dialog.json",
    "https://ui.ballmac.com/r/field.json",
    "https://ui.ballmac.com/r/input.json",
    "https://ui.ballmac.com/r/password-input.json",
    "https://ui.ballmac.com/r/segmented-control.json",
    "https://ui.ballmac.com/r/select.json",
    "https://ui.ballmac.com/r/textarea.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/settings-1/settings-1.tsx",
      "content": "// Ballmac UI: Settings 1. https://ui.ballmac.com/blocks/settings-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Loader2, ShieldCheck, Trash2 } from \"lucide-react\"\n\nimport { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from \"@/components/ballmac/alert-dialog\"\nimport { Avatar, AvatarFallback } from \"@/components/ballmac/avatar\"\nimport { Badge } from \"@/components/ballmac/badge\"\nimport { Button, buttonVariants } from \"@/components/ballmac/button\"\nimport { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from \"@/components/ballmac/dialog\"\nimport { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, useFieldControl } from \"@/components/ballmac/field\"\nimport { Input } from \"@/components/ballmac/input\"\nimport { PasswordInput } from \"@/components/ballmac/password-input\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ballmac/select\"\nimport { Textarea } from \"@/components/ballmac/textarea\"\nimport { cn } from \"@/lib/utils\"\n\ntype Settings1Values = {\n  name: string\n  username: string\n  bio: string\n  language: string\n  timezone: string\n  theme: \"light\" | \"dark\" | \"system\"\n}\n\ntype Settings1Props = Omit<React.ComponentProps<\"section\">, \"title\" | \"onSubmit\"> & {\n  /** Page heading. */\n  title?: string\n  /** One line under the heading. */\n  description?: string\n  /** Saved values. Edit them and the Save bar appears. */\n  defaultValues?: Settings1Values\n  /** The account email, shown read-only with a verified badge. */\n  email?: string\n  /** Usernames that are already taken, used for the availability hint. */\n  takenUsernames?: string[]\n  /** Languages offered. */\n  languages?: string[]\n  /** Time zones offered. */\n  timezones?: string[]\n  /** Called with the new values. Throw to show an error; resolve to confirm. Defaults to a short simulated save. */\n  onSave?: (values: Settings1Values) => void | Promise<void>\n  /** Called after the person confirms deleting their account. */\n  onDeleteAccount?: () => void | Promise<void>\n}\n\nconst defaults: Settings1Values = {\n  name: \"Jordan Lee\",\n  username: \"jordanlee\",\n  bio: \"Finance lead who likes quiet dashboards.\",\n  language: \"English\",\n  timezone: \"Europe/Lisbon (WET)\",\n  theme: \"system\",\n}\n\nfunction FieldInput(props: React.ComponentProps<typeof Input>) {\n  return <Input {...useFieldControl()} {...props} />\n}\nfunction FieldPassword(props: React.ComponentProps<typeof PasswordInput>) {\n  return <PasswordInput {...useFieldControl()} {...props} />\n}\nfunction FieldTextarea(props: React.ComponentProps<typeof Textarea>) {\n  return <Textarea {...useFieldControl()} {...props} />\n}\nfunction FieldSelectTrigger(props: React.ComponentProps<typeof SelectTrigger>) {\n  const { id, disabled, ...aria } = useFieldControl()\n  return <SelectTrigger id={id} disabled={disabled} className=\"w-full\" {...aria} {...props} />\n}\n\n/** A settings group: title and help text on the left, the controls in a card on the right. */\nfunction Group({ title, description, children, tone }: { title: string; description: string; children: React.ReactNode; tone?: \"danger\" }) {\n  return (\n    <div className=\"grid gap-4 border-t py-8 first:border-t-0 first:pt-0 lg:grid-cols-[16rem_1fr] lg:gap-10\">\n      <div>\n        <h3 className=\"text-sm font-semibold\">{title}</h3>\n        <p className=\"text-muted-foreground mt-1 text-sm text-pretty\">{description}</p>\n      </div>\n      <div className={cn(\"bg-card min-w-0 rounded-2xl border p-5 sm:p-6\", tone === \"danger\" && \"border-destructive/40\")}>{children}</div>\n    </div>\n  )\n}\n\nfunction Settings1({\n  title = \"Account settings\",\n  description = \"Manage your profile, sign-in details and preferences.\",\n  defaultValues = defaults,\n  email = \"jordan@acme.com\",\n  takenUsernames = [\"admin\", \"support\", \"jordan\"],\n  languages = [\"English\", \"Español\", \"Português\", \"Deutsch\", \"Français\"],\n  timezones = [\"Europe/Lisbon (WET)\", \"America/New_York (EST)\", \"America/Los_Angeles (PST)\", \"Asia/Tokyo (JST)\"],\n  onSave,\n  onDeleteAccount,\n  className,\n  ...props\n}: Settings1Props) {\n  const [saved, setSaved] = React.useState(defaultValues)\n  const [v, setV] = React.useState(defaultValues)\n  const [state, setState] = React.useState<\"idle\" | \"saving\" | \"saved\" | \"error\">(\"idle\")\n  const [confirm, setConfirm] = React.useState(\"\")\n  const [pwOpen, setPwOpen] = React.useState(false)\n  const [pw, setPw] = React.useState({ current: \"\", next: \"\" })\n  const dirty = (Object.keys(v) as (keyof Settings1Values)[]).some((k) => v[k] !== saved[k])\n  const set = <K extends keyof Settings1Values>(k: K, value: Settings1Values[K]) => {\n    setV((p) => ({ ...p, [k]: value }))\n    if (state === \"saved\" || state === \"error\") setState(\"idle\")\n  }\n  const nameError = !v.name.trim() ? \"Your name can’t be empty.\" : undefined\n  const handle = v.username.trim().toLowerCase()\n  const taken = handle !== saved.username && takenUsernames.includes(handle)\n  const usernameError = !handle ? \"Choose a username.\" : taken ? `@${handle} is already taken.` : undefined\n  const invalid = !!nameError || !!usernameError\n\n  async function save() {\n    if (invalid) return\n    setState(\"saving\")\n    try {\n      if (onSave) await onSave(v)\n      else await new Promise((r) => setTimeout(r, 800))\n      setSaved(v)\n      setState(\"saved\")\n    } catch {\n      setState(\"error\")\n    }\n  }\n\n  const initials = v.name.split(\" \").map((w) => w[0]).join(\"\").slice(0, 2)\n  return (\n    <section data-slot=\"settings-1\" className={cn(\"mx-auto w-full max-w-4xl px-4 py-8 sm:px-6\", className)} {...props}>\n      <h2 className=\"text-2xl font-semibold tracking-[-0.03em]\">{title}</h2>\n      <p className=\"text-muted-foreground mt-1 text-sm\">{description}</p>\n\n      <div className=\"mt-8\">\n        <Group title=\"Profile\" description=\"This is how you appear to your teammates.\">\n          <div className=\"flex items-center gap-4\">\n            <Avatar className=\"size-16\"><AvatarFallback className=\"bg-chart-1/25 text-foreground text-lg font-semibold\">{initials}</AvatarFallback></Avatar>\n            <div className=\"flex flex-wrap gap-2\">\n              <Button variant=\"outline\" size=\"sm\">Change photo</Button>\n              <Button variant=\"ghost\" size=\"sm\">Remove</Button>\n            </div>\n          </div>\n          <FieldGroup className=\"mt-6\">\n            <Field invalid={!!nameError}>\n              <FieldLabel required>Full name</FieldLabel>\n              <FieldInput autoComplete=\"name\" value={v.name} onChange={(e) => set(\"name\", e.target.value)} />\n              <FieldError errors={[nameError]} />\n            </Field>\n            <Field invalid={!!usernameError}>\n              <FieldLabel required>Username</FieldLabel>\n              <div className=\"flex\">\n                <span className=\"bg-muted text-muted-foreground flex items-center rounded-s-md border border-e-0 px-3 text-sm\">@</span>\n                <FieldInput className=\"rounded-s-none\" autoCapitalize=\"none\" spellCheck={false} value={v.username} onChange={(e) => set(\"username\", e.target.value.replace(/\\s/g, \"\"))} />\n              </div>\n              <FieldError errors={[usernameError]} />\n              {!usernameError && handle !== saved.username && <p role=\"status\" className=\"text-sm\"><Check className=\"text-chart-2 me-1 inline size-4\" aria-hidden=\"true\" />@{handle} is available.</p>}\n            </Field>\n            <Field>\n              <FieldLabel>Bio</FieldLabel>\n              <FieldTextarea rows={3} maxLength={160} value={v.bio} onChange={(e) => set(\"bio\", e.target.value)} />\n              <FieldDescription className=\"text-xs tabular-nums\">{v.bio.length}/160</FieldDescription>\n            </Field>\n          </FieldGroup>\n        </Group>\n\n        <Group title=\"Sign-in\" description=\"Your email and password.\">\n          <div className=\"flex flex-wrap items-center justify-between gap-3\">\n            <div>\n              <p className=\"text-sm font-medium\">Email</p>\n              <p className=\"text-muted-foreground mt-0.5 flex items-center gap-2 text-sm break-all\">{email} <Badge status=\"success\">Verified</Badge></p>\n            </div>\n            <Button variant=\"outline\" size=\"sm\">Change email</Button>\n          </div>\n          <div className=\"my-5 border-t\" />\n          <div className=\"flex flex-wrap items-center justify-between gap-3\">\n            <div>\n              <p className=\"text-sm font-medium\">Password</p>\n              <p className=\"text-muted-foreground mt-0.5 text-sm\">Last changed 3 months ago</p>\n            </div>\n            <Dialog open={pwOpen} onOpenChange={setPwOpen}>\n              <DialogTrigger className={buttonVariants({ variant: \"outline\", size: \"sm\" })}>Change password</DialogTrigger>\n              <DialogContent>\n                <DialogHeader>\n                  <DialogTitle>Change password</DialogTitle>\n                  <DialogDescription>You’ll stay signed in on this device and be signed out everywhere else.</DialogDescription>\n                </DialogHeader>\n                <FieldGroup>\n                  <Field><FieldLabel>Current password</FieldLabel><FieldPassword label=\"Current password\" showStrength={false} autoComplete=\"current-password\" value={pw.current} onValueChange={(current) => setPw((p) => ({ ...p, current }))} /></Field>\n                  <Field><FieldLabel>New password</FieldLabel><FieldPassword label=\"New password\" value={pw.next} onValueChange={(next) => setPw((p) => ({ ...p, next }))} /></Field>\n                </FieldGroup>\n                <DialogFooter>\n                  <Button variant=\"ghost\" onClick={() => setPwOpen(false)}>Cancel</Button>\n                  <Button disabled={!pw.current || pw.next.length < 8} onClick={() => { setPwOpen(false); setPw({ current: \"\", next: \"\" }) }}>Update password</Button>\n                </DialogFooter>\n              </DialogContent>\n            </Dialog>\n          </div>\n          <p className=\"text-muted-foreground mt-5 flex items-center gap-2 text-xs\"><ShieldCheck className=\"size-4\" aria-hidden=\"true\" />Two-factor authentication is on.</p>\n        </Group>\n\n        <Group title=\"Preferences\" description=\"Language, time zone and appearance.\">\n          <FieldGroup>\n            <div className=\"grid gap-5 sm:grid-cols-2\">\n              <Field>\n                <FieldLabel>Language</FieldLabel>\n                <Select value={v.language} onValueChange={(x) => set(\"language\", x)}>\n                  <FieldSelectTrigger><SelectValue /></FieldSelectTrigger>\n                  <SelectContent>{languages.map((l) => <SelectItem key={l} value={l}>{l}</SelectItem>)}</SelectContent>\n                </Select>\n              </Field>\n              <Field>\n                <FieldLabel>Time zone</FieldLabel>\n                <Select value={v.timezone} onValueChange={(x) => set(\"timezone\", x)}>\n                  <FieldSelectTrigger><SelectValue /></FieldSelectTrigger>\n                  <SelectContent>{timezones.map((l) => <SelectItem key={l} value={l}>{l}</SelectItem>)}</SelectContent>\n                </Select>\n              </Field>\n            </div>\n            <div>\n              <p className=\"mb-2 text-sm font-medium\">Appearance</p>\n              <SegmentedControl aria-label=\"Appearance\" value={v.theme} onValueChange={(x) => set(\"theme\", x as Settings1Values[\"theme\"])}>\n                <SegmentedControlItem value=\"light\">Light</SegmentedControlItem>\n                <SegmentedControlItem value=\"dark\">Dark</SegmentedControlItem>\n                <SegmentedControlItem value=\"system\">System</SegmentedControlItem>\n              </SegmentedControl>\n            </div>\n          </FieldGroup>\n        </Group>\n\n        <Group title=\"Danger zone\" description=\"Permanent actions that can’t be undone.\" tone=\"danger\">\n          <div className=\"flex flex-wrap items-center justify-between gap-3\">\n            <div>\n              <p className=\"text-sm font-medium\">Delete account</p>\n              <p className=\"text-muted-foreground mt-0.5 text-sm\">Removes your profile and all personal data.</p>\n            </div>\n            <AlertDialog onOpenChange={() => setConfirm(\"\")}>\n              <AlertDialogTrigger className={buttonVariants({ variant: \"outline\", size: \"sm\", className: \"text-destructive border-destructive/40\" })}><Trash2 /> Delete account</AlertDialogTrigger>\n              <AlertDialogContent>\n                <AlertDialogHeader>\n                  <AlertDialogTitle>Delete your account?</AlertDialogTitle>\n                  <AlertDialogDescription>This permanently deletes {v.name}’s profile and personal data. Type <span className=\"text-foreground font-mono\">delete</span> to confirm.</AlertDialogDescription>\n                </AlertDialogHeader>\n                <Input aria-label=\"Type delete to confirm\" value={confirm} onChange={(e) => setConfirm(e.target.value)} placeholder=\"delete\" autoComplete=\"off\" />\n                <AlertDialogFooter>\n                  <AlertDialogCancel>Keep my account</AlertDialogCancel>\n                  <AlertDialogAction destructive disabled={confirm !== \"delete\"} onClick={() => onDeleteAccount?.()}>Delete account</AlertDialogAction>\n                </AlertDialogFooter>\n              </AlertDialogContent>\n            </AlertDialog>\n          </div>\n        </Group>\n      </div>\n\n      {/* The save bar appears only when something changed, and stays on screen while you scroll. */}\n      <div\n        aria-hidden={!dirty && state !== \"saved\" ? true : undefined}\n        className={cn(\n          \"sticky bottom-4 z-10 mt-4 flex items-center justify-between gap-3 rounded-2xl border bg-background/90 p-3 ps-5 shadow-[0_20px_50px_-20px_rgb(0_0_0/0.35)] backdrop-blur-xl transition-all duration-300 motion-reduce:transition-none\",\n          dirty || state === \"saved\" || state === \"error\" ? \"translate-y-0 opacity-100\" : \"pointer-events-none translate-y-4 opacity-0\"\n        )}\n      >\n        <p role=\"status\" className=\"flex items-center gap-2 text-sm\">\n          {state === \"saving\" && <><Loader2 className=\"size-4 animate-spin motion-reduce:animate-none\" aria-hidden=\"true\" />Saving…</>}\n          {state === \"saved\" && !dirty && <><Check className=\"text-chart-2 size-4\" aria-hidden=\"true\" />All changes saved</>}\n          {state === \"error\" && <span className=\"text-destructive\">We couldn’t save your changes. Try again.</span>}\n          {dirty && state !== \"saving\" && state !== \"error\" && \"You have unsaved changes\"}\n        </p>\n        {dirty && (\n          <div className=\"flex gap-2\">\n            <Button variant=\"ghost\" size=\"sm\" disabled={state === \"saving\"} onClick={() => { setV(saved); setState(\"idle\") }}>Discard</Button>\n            <Button size=\"sm\" shape=\"pill\" disabled={invalid} loading={state === \"saving\"} onClick={save}>Save changes</Button>\n          </div>\n        )}\n      </div>\n    </section>\n  )\n}\n\nexport { Settings1, type Settings1Props, type Settings1Values }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/settings-1/settings-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "settings",
      "profile",
      "account",
      "preferences",
      "save bar",
      "danger zone"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "Profile and account settings. Pass defaultValues, email, onSave(values) (throw to show an error) and onDeleteAccount; the dirty-state save bar, validation and availability hint are built in.",
      "whenToUse": [
        "Account and profile pages",
        "Any settings screen where edits are saved together"
      ],
      "whenNotToUse": [
        "Toggle-style settings that save instantly (use settings-2 or settings-panel)"
      ],
      "composesWith": [
        "app-shell-1",
        "settings-2",
        "settings-3",
        "billing-1"
      ],
      "a11y": [
        {
          "keys": "Edit any field",
          "action": "The save bar slides in; its state (unsaved, saving, saved, error) is announced politely"
        },
        {
          "keys": "Delete account",
          "action": "An alert dialog asks you to type 'delete'; the confirm button stays disabled until you do"
        },
        {
          "keys": "Invalid name or username",
          "action": "Errors are announced and linked to the field; saving is blocked"
        }
      ],
      "customization": [
        "defaultValues, email, takenUsernames, languages, timezones",
        "onSave(values), onDeleteAccount()"
      ]
    },
    "examples": [
      "settings-1-demo",
      "settings-1-save"
    ],
    "url": "https://ui.ballmac.com/components/settings-1"
  },
  "categories": [
    "blocks",
    "settings"
  ],
  "type": "registry:block"
}