{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "settings-6",
  "title": "Settings 6: data and privacy",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A data and privacy page: request an export in JSON or CSV and watch it prepare, set how long messages are kept, and delete the account behind a typed confirmation with a grace period you can cancel.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/alert-dialog.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/checkbox.json",
    "https://ui.ballmac.com/r/input.json",
    "https://ui.ballmac.com/r/progress.json",
    "https://ui.ballmac.com/r/segmented-control.json",
    "https://ui.ballmac.com/r/select.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/switch.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/settings-6/settings-6.tsx",
      "content": "// Ballmac UI: Settings 6. https://ui.ballmac.com/blocks/settings-6\n\"use client\"\n\nimport * as React from \"react\"\nimport { CalendarClock, CircleAlert, CircleCheck, Download, FileArchive, Hourglass, Loader2, ShieldCheck, Trash2, Undo2 } from \"lucide-react\"\n\nimport { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from \"@/components/ballmac/alert-dialog\"\nimport { Button } from \"@/components/ballmac/button\"\nimport { Checkbox } from \"@/components/ballmac/checkbox\"\nimport { Input } from \"@/components/ballmac/input\"\nimport { Progress } from \"@/components/ballmac/progress\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ballmac/select\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { Switch } from \"@/components/ballmac/switch\"\nimport { cn } from \"@/lib/utils\"\n\ntype Settings6Scope = \"profile\" | \"projects\" | \"messages\" | \"billing\"\ntype Settings6Format = \"json\" | \"csv\"\ntype Settings6Retention = \"30d\" | \"1y\" | \"forever\"\n\ntype Settings6Export = {\n  id: string\n  /** Already written for people (\"14 October, 09:42\"). */\n  requested: string\n  /** What it contains, in words (\"Profile, projects and messages\"). */\n  contents: string\n  format: Settings6Format\n  status: \"preparing\" | \"ready\" | \"failed\" | \"expired\"\n  /** 0 to 100 while preparing. */\n  progress?: number\n  /** \"18 MB\", when ready. */\n  size?: string\n  /** \"Expires in 6 days\", when ready. */\n  expires?: string\n}\n\ntype Settings6Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    title?: string\n    description?: string\n    /** The account email, which the person types to confirm deletion. */\n    email?: string\n    /** Past and running exports. Update the list from your server as jobs progress. */\n    exports?: Settings6Export[]\n    /** Called with what to include and the format. Return a promise; reject to show the message. Without it, a preparing job is simulated for demos. */\n    onRequestExport?: (request: { scope: Settings6Scope[]; format: Settings6Format }) => void | Promise<void>\n    onDownload?: (item: Settings6Export) => void\n    /** How long messages are kept (controlled by onRetentionChange). */\n    retention?: Settings6Retention\n    onRetentionChange?: (value: Settings6Retention) => void\n    /** Whether anonymous product analytics are shared. */\n    analytics?: boolean\n    onAnalyticsChange?: (value: boolean) => void\n    /** Set when deletion is already scheduled (\"12 November\"). */\n    deletionDate?: string\n    /** Called when deletion is confirmed. Return a promise; reject to show the message. */\n    onDeleteAccount?: () => void | Promise<void>\n    onCancelDeletion?: () => void | Promise<void>\n  }\n\nconst scopes: { id: Settings6Scope; label: string; hint: string }[] = [\n  { id: \"profile\", label: \"Profile and settings\", hint: \"Name, email, preferences\" },\n  { id: \"projects\", label: \"Projects and files\", hint: \"Everything you created\" },\n  { id: \"messages\", label: \"Messages and comments\", hint: \"Conversations you took part in\" },\n  { id: \"billing\", label: \"Billing history\", hint: \"Invoices and payments\" },\n]\n\nconst sampleExports: Settings6Export[] = [\n  { id: \"e2\", requested: \"12 October, 16:20\", contents: \"Profile, projects and messages\", format: \"json\", status: \"ready\", size: \"18 MB\", expires: \"Expires in 4 days\" },\n  { id: \"e1\", requested: \"2 September, 09:05\", contents: \"Billing history\", format: \"csv\", status: \"expired\" },\n]\n\nconst retentionLabel: Record<Settings6Retention, string> = { \"30d\": \"30 days\", \"1y\": \"1 year\", forever: \"Until I delete them\" }\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-6\">\n      {[0, 1, 2].map((i) => (\n        <div key={i} className=\"bg-card rounded-2xl border p-6\">\n          <Skeleton className=\"h-5 w-40\" />\n          <Skeleton className=\"mt-3 h-4 w-3/4\" />\n          <Skeleton className=\"mt-6 h-9 w-48\" />\n        </div>\n      ))}\n    </div>\n  )\n}\n\nfunction Card({ id, icon: Icon, title, children, danger }: { id: string; icon: React.ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>; title: string; children: React.ReactNode; danger?: boolean }) {\n  return (\n    <section aria-labelledby={id} className={cn(\"bg-card rounded-2xl border p-5 sm:p-6\", danger && \"border-destructive/40\")}>\n      <h2 id={id} className=\"flex items-center gap-2.5 text-base font-semibold tracking-tight\">\n        <span aria-hidden=\"true\" className={cn(\"bg-muted flex size-8 items-center justify-center rounded-lg\", danger && \"bg-destructive/10 text-destructive\")}>\n          <Icon aria-hidden className=\"size-4\" />\n        </span>\n        {title}\n      </h2>\n      {children}\n    </section>\n  )\n}\n\n/**\n * A data and privacy page: ask for a copy of your data (pick what to include, watch it prepare, download it before it expires), choose how\n * long messages are kept, and delete the account behind a typed confirmation with a grace period you can cancel.\n */\nfunction Settings6({\n  title = \"Data and privacy\",\n  description = \"Take your data with you, decide how long we keep it, or remove it.\",\n  email = \"priya@acme.com\",\n  exports: exportsProp,\n  onRequestExport,\n  onDownload,\n  retention: retentionProp,\n  onRetentionChange,\n  analytics: analyticsProp,\n  onAnalyticsChange,\n  deletionDate: deletionProp,\n  onDeleteAccount,\n  onCancelDeletion,\n  state = \"ready\",\n  onRetry,\n  errorState,\n  className,\n  ...props\n}: Settings6Props) {\n  const id = React.useId()\n  const [list, setList] = React.useState<Settings6Export[]>(exportsProp ?? sampleExports)\n  React.useEffect(() => {\n    if (exportsProp) setList(exportsProp)\n  }, [exportsProp])\n  const [picked, setPicked] = React.useState<Set<Settings6Scope>>(new Set([\"profile\", \"projects\"]))\n  const [format, setFormat] = React.useState<Settings6Format>(\"json\")\n  const [requesting, setRequesting] = React.useState(false)\n  const [exportError, setExportError] = React.useState<string>()\n  const [retention, setRetention] = React.useState<Settings6Retention>(retentionProp ?? \"1y\")\n  const [analytics, setAnalytics] = React.useState(analyticsProp ?? true)\n  const [deletionDate, setDeletionDate] = React.useState(deletionProp)\n  const [confirmOpen, setConfirmOpen] = React.useState(false)\n  const [typed, setTyped] = React.useState(\"\")\n  const [deleting, setDeleting] = React.useState(false)\n  const [deleteError, setDeleteError] = React.useState<string>()\n  const [announce, setAnnounce] = React.useState(\"\")\n  const deleteTrigger = React.useRef<HTMLButtonElement>(null)\n\n  React.useEffect(() => setDeletionDate(deletionProp), [deletionProp])\n  React.useEffect(() => {\n    if (retentionProp) setRetention(retentionProp)\n  }, [retentionProp])\n  React.useEffect(() => {\n    if (analyticsProp !== undefined) setAnalytics(analyticsProp)\n  }, [analyticsProp])\n\n  // Without a server, a request is simulated so the whole path from asking to downloading can be seen.\n  React.useEffect(() => {\n    if (onRequestExport || exportsProp) return\n    if (!list.some((e) => e.status === \"preparing\")) return\n    const t = setInterval(() => {\n      setList((l) =>\n        l.map((e) => {\n          if (e.status !== \"preparing\") return e\n          const p = Math.min(100, (e.progress ?? 0) + 18)\n          return p >= 100 ? { ...e, status: \"ready\", progress: 100, size: \"12 MB\", expires: \"Expires in 7 days\" } : { ...e, progress: p }\n        })\n      )\n    }, 700)\n    return () => clearInterval(t)\n  }, [list, onRequestExport, exportsProp])\n\n  const toggleScope = (s: Settings6Scope, on: boolean) =>\n    setPicked((p) => {\n      const n = new Set(p)\n      if (on) n.add(s)\n      else n.delete(s)\n      return n\n    })\n\n  async function request() {\n    if (!picked.size || requesting) return\n    const scope = scopes.filter((s) => picked.has(s.id)).map((s) => s.id)\n    setExportError(undefined)\n    setRequesting(true)\n    try {\n      if (onRequestExport) await onRequestExport({ scope, format })\n      else {\n        const names = scopes.filter((s) => picked.has(s.id)).map((s) => s.label.split(\" and \")[0]!.toLowerCase())\n        const contents = names.length > 1 ? `${names.slice(0, -1).join(\", \")} and ${names.at(-1)}` : names[0]!\n        setList((l) => [{ id: `e${Date.now()}`, requested: \"Just now\", contents: contents[0]!.toUpperCase() + contents.slice(1), format, status: \"preparing\", progress: 4 }, ...l])\n      }\n      setAnnounce(\"Your export was requested. We will email you when it is ready.\")\n    } catch (e) {\n      setExportError(e instanceof Error ? e.message : \"We could not start the export. Try again.\")\n    } finally {\n      setRequesting(false)\n    }\n  }\n\n  async function confirmDelete() {\n    setDeleteError(undefined)\n    setDeleting(true)\n    try {\n      await onDeleteAccount?.()\n      setDeletionDate((d) => d ?? \"in 30 days\")\n      setConfirmOpen(false)\n      setTyped(\"\")\n      setAnnounce(\"Deletion confirmed. You have 30 days to change your mind.\")\n    } catch (e) {\n      setDeleteError(e instanceof Error ? e.message : \"We could not schedule the deletion. Nothing was changed.\")\n    } finally {\n      setDeleting(false)\n    }\n  }\n\n  const busy = list.some((e) => e.status === \"preparing\")\n\n  const content = (\n    <div className=\"grid gap-6\">\n      <Card id={`${id}-export`} icon={FileArchive} title=\"Export your data\">\n        <p className=\"text-muted-foreground mt-2 max-w-xl text-sm text-pretty\">We prepare a download and email you when it is ready. Links work for a week.</p>\n        <fieldset className=\"mt-5\">\n          <legend className=\"text-sm font-medium\">Include</legend>\n          <div className=\"mt-3 grid gap-2 sm:grid-cols-2\">\n            {scopes.map((s) => (\n              <label key={s.id} className={cn(\"hover:bg-accent/50 flex cursor-pointer items-start gap-3 rounded-xl border p-3 transition-colors motion-reduce:transition-none\", picked.has(s.id) && \"border-foreground/40 bg-accent/30\")}>\n                <Checkbox checked={picked.has(s.id)} onCheckedChange={(v) => toggleScope(s.id, v === true)} className=\"mt-0.5\" />\n                <span className=\"min-w-0\">\n                  <span className=\"block text-sm font-medium\">{s.label}</span>\n                  <span className=\"text-muted-foreground block text-xs\">{s.hint}</span>\n                </span>\n              </label>\n            ))}\n          </div>\n        </fieldset>\n        <div className=\"mt-5 flex flex-wrap items-end justify-between gap-4\">\n          <div>\n            <p id={`${id}-format`} className=\"mb-2 text-sm font-medium\">\n              Format\n            </p>\n            <SegmentedControl value={format} onValueChange={(v) => setFormat(v as Settings6Format)} aria-labelledby={`${id}-format`} size=\"sm\">\n              <SegmentedControlItem value=\"json\">JSON</SegmentedControlItem>\n              <SegmentedControlItem value=\"csv\">CSV</SegmentedControlItem>\n            </SegmentedControl>\n          </div>\n          <Button onClick={request} disabled={!picked.size || requesting} aria-describedby={!picked.size ? `${id}-none` : undefined}>\n            {requesting ? <Loader2 aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" /> : <Download aria-hidden=\"true\" />}\n            {requesting ? \"Requesting\" : \"Request export\"}\n          </Button>\n        </div>\n        {!picked.size ? (\n          <p id={`${id}-none`} className=\"text-muted-foreground mt-3 text-sm\">\n            Choose at least one thing to include.\n          </p>\n        ) : null}\n        {exportError ? (\n          <p role=\"alert\" className=\"text-destructive mt-3 flex items-center gap-2 text-sm\">\n            <CircleAlert aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n            {exportError}\n          </p>\n        ) : null}\n\n        {list.length ? (\n          <div className=\"mt-6 border-t pt-5\">\n            <h3 className=\"text-sm font-medium\">Your exports</h3>\n            <ul className=\"mt-3 grid gap-2.5\">\n              {list.map((e) => (\n                <li key={e.id} className=\"flex flex-wrap items-center gap-x-4 gap-y-3 rounded-xl border p-3.5\">\n                  <span aria-hidden=\"true\" className={cn(\"flex size-9 shrink-0 items-center justify-center rounded-lg\", e.status === \"ready\" ? \"bg-chart-2/15 text-chart-2\" : e.status === \"failed\" ? \"bg-destructive/10 text-destructive\" : \"bg-muted text-muted-foreground\")}>\n                    {e.status === \"ready\" ? <CircleCheck className=\"size-4\" /> : e.status === \"failed\" ? <CircleAlert className=\"size-4\" /> : e.status === \"preparing\" ? <Hourglass className=\"size-4\" /> : <CalendarClock className=\"size-4\" />}\n                  </span>\n                  <div className=\"min-w-0 flex-1 basis-48\">\n                    <p className=\"text-sm font-medium\">\n                      {e.contents} <span className=\"text-muted-foreground font-normal uppercase\">· {e.format}</span>\n                    </p>\n                    <p className=\"text-muted-foreground mt-0.5 text-xs\">\n                      Requested {e.requested}\n                      {e.status === \"ready\" ? ` · ${e.size} · ${e.expires}` : e.status === \"expired\" ? \" · Link expired\" : e.status === \"failed\" ? \" · Something went wrong, ask again\" : \"\"}\n                    </p>\n                    {e.status === \"preparing\" ? <Progress value={e.progress ?? 0} aria-label={`Preparing ${e.contents}`} className=\"mt-2.5 max-w-xs\" /> : null}\n                  </div>\n                  {e.status === \"preparing\" ? <span className=\"text-muted-foreground text-sm tabular-nums\">{e.progress ?? 0}%</span> : null}\n                  {e.status === \"ready\" ? (\n                    <Button variant=\"outline\" size=\"sm\" onClick={() => onDownload?.(e)}>\n                      <Download aria-hidden=\"true\" />\n                      Download\n                    </Button>\n                  ) : null}\n                </li>\n              ))}\n            </ul>\n          </div>\n        ) : null}\n      </Card>\n\n      <Card id={`${id}-keep`} icon={ShieldCheck} title=\"What we keep\">\n        <div className=\"mt-4 grid gap-5\">\n          <div className=\"flex flex-wrap items-center justify-between gap-3\">\n            <div className=\"max-w-md\">\n              <p id={`${id}-ret`} className=\"text-sm font-medium\">\n                Keep my messages for\n              </p>\n              <p className=\"text-muted-foreground mt-0.5 text-sm text-pretty\">Older messages are deleted for good, and cannot be recovered.</p>\n            </div>\n            <Select\n              value={retention}\n              onValueChange={(v) => {\n                setRetention(v as Settings6Retention)\n                onRetentionChange?.(v as Settings6Retention)\n              }}\n            >\n              <SelectTrigger aria-labelledby={`${id}-ret`} className=\"w-48\">\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                {(Object.keys(retentionLabel) as Settings6Retention[]).map((r) => (\n                  <SelectItem key={r} value={r}>\n                    {retentionLabel[r]}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n          <div className=\"flex items-center justify-between gap-4 border-t pt-5\">\n            <div className=\"max-w-md\">\n              <p id={`${id}-an`} className=\"text-sm font-medium\">\n                Share anonymous usage data\n              </p>\n              <p className=\"text-muted-foreground mt-0.5 text-sm text-pretty\">Helps us fix what is slow. Never includes your content.</p>\n            </div>\n            <Switch\n              checked={analytics}\n              onCheckedChange={(v) => {\n                setAnalytics(v)\n                onAnalyticsChange?.(v)\n              }}\n              aria-labelledby={`${id}-an`}\n            />\n          </div>\n        </div>\n      </Card>\n\n      <Card id={`${id}-delete`} icon={Trash2} title=\"Delete your account\" danger>\n        {deletionDate ? (\n          <div role=\"status\" className=\"bg-destructive/5 border-destructive/30 mt-4 flex flex-wrap items-center justify-between gap-4 rounded-xl border p-4\">\n            <div className=\"min-w-0 max-w-md\">\n              <p className=\"text-sm font-medium\">Your account is scheduled for deletion {deletionDate.startsWith(\"in\") ? deletionDate : `on ${deletionDate}`}.</p>\n              <p className=\"text-muted-foreground mt-1 text-sm text-pretty\">Until then you can sign in and change your mind. After that, everything is removed for good.</p>\n            </div>\n            <Button\n              variant=\"outline\"\n              onClick={async () => {\n                await onCancelDeletion?.()\n                setDeletionDate(undefined)\n                setAnnounce(\"Deletion cancelled. Your account stays as it is.\")\n              }}\n            >\n              <Undo2 aria-hidden=\"true\" className=\"rtl:-scale-x-100\" />\n              Keep my account\n            </Button>\n          </div>\n        ) : (\n          <>\n            <p className=\"text-muted-foreground mt-2 max-w-xl text-sm text-pretty\">Removes your profile, projects and messages. We wait 30 days before deleting anything, so you can change your mind. Export first if you want a copy.</p>\n            <Button ref={deleteTrigger} variant=\"destructive\" className=\"mt-5\" onClick={() => setConfirmOpen(true)}>\n              <Trash2 aria-hidden=\"true\" />\n              Delete account\n            </Button>\n          </>\n        )}\n      </Card>\n\n      <AlertDialog\n        open={confirmOpen}\n        onOpenChange={(o) => {\n          if (deleting) return\n          setConfirmOpen(o)\n          if (!o) {\n            setTyped(\"\")\n            setDeleteError(undefined)\n          }\n        }}\n      >\n        <AlertDialogContent\n          onCloseAutoFocus={(e) => {\n            e.preventDefault()\n            deleteTrigger.current?.focus()\n          }}\n        >\n          <AlertDialogHeader>\n            <AlertDialogTitle>Delete this account?</AlertDialogTitle>\n            <AlertDialogDescription>\n              {busy ? \"An export is still being prepared; it will be lost. \" : \"\"}After 30 days your projects, messages and settings are removed for good. To confirm, type <strong className=\"text-foreground\">{email}</strong>.\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <Input value={typed} onChange={(e) => setTyped(e.target.value)} aria-label=\"Type your email to confirm\" autoComplete=\"off\" placeholder={email} aria-invalid={!!deleteError} />\n          {deleteError ? (\n            <p role=\"alert\" className=\"text-destructive text-sm\">\n              {deleteError}\n            </p>\n          ) : null}\n          <AlertDialogFooter>\n            <AlertDialogCancel disabled={deleting}>Keep my account</AlertDialogCancel>\n            <AlertDialogAction\n              destructive\n              disabled={typed.trim().toLowerCase() !== email.toLowerCase() || deleting}\n              onClick={(e) => {\n                e.preventDefault()\n                void confirmDelete()\n              }}\n            >\n              {deleting ? <Loader2 aria-hidden=\"true\" className=\"me-2 size-4 animate-spin motion-reduce:animate-none\" /> : null}\n              {deleting ? \"Scheduling\" : \"Delete account\"}\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </div>\n  )\n\n  return (\n    <section data-slot=\"settings-6\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-3xl px-4 py-10 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-8\">\n        <h1 id={`${id}-title`} className=\"text-3xl font-semibold tracking-[-0.03em] text-balance\">\n          {title}\n        </h1>\n        <p className=\"text-muted-foreground mt-2 text-base text-pretty\">{description}</p>\n      </header>\n      <StateView state={state === \"loading\" || state === \"error\" ? state : \"ready\"} onRetry={onRetry} label=\"Loading your data settings\" loading={<Skel />} error={{ title: errorState?.title ?? \"Your data settings could not be loaded\", description: errorState?.description ?? \"Nothing was changed. Check your connection and try again.\", ...errorState }}>\n        {content}\n      </StateView>\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </p>\n    </section>\n  )\n}\n\nexport { Settings6, type Settings6Props, type Settings6Export, type Settings6Scope, type Settings6Format, type Settings6Retention }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/settings-6/settings-6.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "settings",
      "privacy",
      "gdpr",
      "data export",
      "delete account",
      "retention",
      "compliance",
      "download my data",
      "account"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "A data and privacy settings page. <Settings6 email exports={[{ id, requested, contents, format: 'json' | 'csv', status: 'preparing' | 'ready' | 'failed' | 'expired', progress, size, expires }]} onRequestExport={({ scope, format }) => Promise} onDownload retention='30d' | '1y' | 'forever' onRetentionChange analytics onAnalyticsChange deletionDate onDeleteAccount onCancelDeletion />. Reject a promise to show its message in place. Without onRequestExport, a request is simulated so a demo shows the whole path.",
      "whenToUse": [
        "Privacy and data-rights pages (GDPR/CCPA access and erasure)",
        "The danger zone of any account settings"
      ],
      "whenNotToUse": [
        "Cookie choices on first visit (use cookie-consent)",
        "Who changed what in the workspace (use settings-5)"
      ],
      "composesWith": [
        "settings-1",
        "settings-5",
        "app-shell-1"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Checkboxes, format, request, then each export's download; every control has a visible label"
        },
        {
          "keys": "Delete",
          "action": "An alert dialog; the confirm button stays disabled until the email is typed exactly, focus returns to Delete account when it closes"
        },
        {
          "keys": "Progress",
          "action": "Each preparing export has a labelled progress bar and a percentage in text"
        }
      ],
      "customization": [
        "Requests, deletion and cancelling are all async; reject to show a message",
        "deletionDate set from the server shows the scheduled state with a way back",
        "Retention and analytics are controlled with value and onChange props"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "settings-6-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton cards under the real heading, announced as loading."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "settings-6-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was changed, with Try again (onRetry). Failed requests show their message in place."
      }
    ],
    "examples": [
      "settings-6-demo",
      "settings-6-deleting",
      "settings-6-loading",
      "settings-6-error"
    ],
    "url": "https://ui.ballmac.com/components/settings-6"
  },
  "categories": [
    "blocks",
    "settings"
  ],
  "type": "registry:block"
}