{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "settings-2",
  "title": "Settings 2: notification preferences",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Notification settings that save as you go: digest frequency as radio cards, a grid of switches for each event and channel (cards on phones), and quiet hours with times and weekdays.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/input.json",
    "https://ui.ballmac.com/r/switch.json",
    "https://ui.ballmac.com/r/toggle-group.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/settings-2/settings-2.tsx",
      "content": "// Ballmac UI: Settings 2. https://ui.ballmac.com/blocks/settings-2\n\"use client\"\n\nimport * as React from \"react\"\nimport { BellRing, Check, Moon } from \"lucide-react\"\n\nimport { Input } from \"@/components/ballmac/input\"\nimport { Switch } from \"@/components/ballmac/switch\"\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ballmac/toggle-group\"\nimport { cn } from \"@/lib/utils\"\n\ntype Settings2Channel = \"email\" | \"push\" | \"inApp\"\n\ntype Settings2Event = {\n  id: string\n  label: string\n  description: string\n}\n\ntype Settings2Values = {\n  digest: string\n  events: Record<string, Record<Settings2Channel, boolean>>\n  quiet: { enabled: boolean; from: string; to: string; days: string[] }\n}\n\ntype Settings2Props = Omit<React.ComponentProps<\"section\">, \"title\" | \"onChange\"> & {\n  /** Page heading. */\n  title?: string\n  /** One line under the heading. */\n  description?: string\n  /** Things people can be notified about. */\n  events?: Settings2Event[]\n  /** Starting values. */\n  defaultValues?: Partial<Settings2Values>\n  /** Called after every change, with all values. Changes save instantly, so there is no Save button. */\n  onChange?: (values: Settings2Values) => void | Promise<void>\n}\n\nconst channels: { id: Settings2Channel; label: string }[] = [\n  { id: \"email\", label: \"Email\" },\n  { id: \"push\", label: \"Push\" },\n  { id: \"inApp\", label: \"In-app\" },\n]\n\nconst digests = [\n  { id: \"realtime\", label: \"Real time\", description: \"An email as things happen\" },\n  { id: \"daily\", label: \"Daily\", description: \"One summary each morning\" },\n  { id: \"weekly\", label: \"Weekly\", description: \"A Monday recap\" },\n  { id: \"never\", label: \"Never\", description: \"Only what you turned on below\" },\n]\n\nconst days = [\n  [\"mon\", \"M\", \"Monday\"],\n  [\"tue\", \"T\", \"Tuesday\"],\n  [\"wed\", \"W\", \"Wednesday\"],\n  [\"thu\", \"T\", \"Thursday\"],\n  [\"fri\", \"F\", \"Friday\"],\n  [\"sat\", \"S\", \"Saturday\"],\n  [\"sun\", \"S\", \"Sunday\"],\n] as const\n\nconst defaultEvents: Settings2Event[] = [\n  { id: \"mentions\", label: \"Mentions\", description: \"Someone @mentions you\" },\n  { id: \"comments\", label: \"Comments\", description: \"Replies on things you own or follow\" },\n  { id: \"assignments\", label: \"Assignments\", description: \"You are assigned a task\" },\n  { id: \"invoices\", label: \"Invoices paid\", description: \"A customer pays an invoice\" },\n  { id: \"product\", label: \"Product news\", description: \"New features and tips\" },\n]\n\nfunction initialEvents(events: Settings2Event[]) {\n  return Object.fromEntries(\n    events.map((e, i) => [e.id, { email: i !== 4, push: i < 3, inApp: true }])\n  ) as Settings2Values[\"events\"]\n}\n\nfunction Settings2({\n  title = \"Notifications\",\n  description = \"Choose what you hear about and where. Changes save as you make them.\",\n  events = defaultEvents,\n  defaultValues,\n  onChange,\n  className,\n  ...props\n}: Settings2Props) {\n  const [values, setValues] = React.useState<Settings2Values>({\n    digest: \"daily\",\n    events: initialEvents(events),\n    quiet: { enabled: true, from: \"22:00\", to: \"07:00\", days: [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\"] },\n    ...defaultValues,\n  })\n  const [status, setStatus] = React.useState<\"idle\" | \"saving\" | \"saved\" | \"error\">(\"idle\")\n  const quietId = React.useId()\n\n  function commit(next: Settings2Values) {\n    setValues(next)\n    setStatus(\"saving\")\n    Promise.resolve(onChange ? onChange(next) : new Promise((r) => setTimeout(r, 350)))\n      .then(() => setStatus(\"saved\"))\n      .catch(() => setStatus(\"error\"))\n  }\n  const toggle = (event: string, channel: Settings2Channel, on: boolean) =>\n    commit({ ...values, events: { ...values.events, [event]: { ...values.events[event], [channel]: on } } })\n\n  return (\n    <section data-slot=\"settings-2\" className={cn(\"mx-auto w-full max-w-4xl px-4 py-8 sm:px-6\", className)} {...props}>\n      <div className=\"flex flex-wrap items-start justify-between gap-3\">\n        <div>\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        </div>\n        <p role=\"status\" className=\"text-muted-foreground flex h-6 items-center gap-1.5 text-sm\">\n          {status === \"saving\" && \"Saving…\"}\n          {status === \"saved\" && <><Check className=\"text-chart-2 size-4\" aria-hidden=\"true\" />Saved</>}\n          {status === \"error\" && <span className=\"text-destructive\">Couldn’t save. Try again.</span>}\n        </p>\n      </div>\n\n      <div className=\"mt-8 space-y-8\">\n        <fieldset>\n          <legend className=\"text-sm font-semibold\">Email digest</legend>\n          <p className=\"text-muted-foreground mt-1 text-sm\">How often you get a summary of activity.</p>\n          <div className=\"mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4\">\n            {digests.map((d) => {\n              const on = values.digest === d.id\n              return (\n                <label key={d.id} className=\"relative cursor-pointer\">\n                  <input type=\"radio\" name=\"digest\" value={d.id} checked={on} onChange={() => commit({ ...values, digest: d.id })} className=\"peer sr-only\" />\n                  <span className=\"peer-focus-visible:ring-ring/50 peer-checked:border-foreground peer-checked:bg-accent/60 hover:bg-accent/40 bg-card flex h-full flex-col rounded-2xl border p-4 transition-colors peer-focus-visible:ring-[3px]\">\n                    <span className=\"flex items-center justify-between text-sm font-medium\">\n                      {d.label}\n                      <span aria-hidden=\"true\" className={cn(\"flex size-4 items-center justify-center rounded-full border\", on && \"bg-foreground text-background border-transparent\")}>{on && <Check className=\"size-3\" />}</span>\n                    </span>\n                    <span className=\"text-muted-foreground mt-1 text-xs\">{d.description}</span>\n                  </span>\n                </label>\n              )\n            })}\n          </div>\n        </fieldset>\n\n        <div>\n          <h3 className=\"flex items-center gap-2 text-sm font-semibold\"><BellRing className=\"size-4\" aria-hidden=\"true\" />What to notify me about</h3>\n          <p className=\"text-muted-foreground mt-1 text-sm\">Turn on each place you want to hear about it.</p>\n\n          {/* Tablets and up: a grid with one switch per event and channel. */}\n          <div className=\"bg-card mt-4 hidden overflow-hidden rounded-2xl border sm:block\">\n            <table className=\"w-full text-start text-sm\">\n              <caption className=\"sr-only\">Notification channels for each event</caption>\n              <thead>\n                <tr className=\"text-muted-foreground bg-muted/40 border-b text-xs\">\n                  <th scope=\"col\" className=\"px-5 py-3 font-medium\">Event</th>\n                  {channels.map((c) => <th key={c.id} scope=\"col\" className=\"w-24 px-3 py-3 text-center font-medium\">{c.label}</th>)}\n                </tr>\n              </thead>\n              <tbody className=\"divide-y\">\n                {events.map((e) => (\n                  <tr key={e.id}>\n                    <th scope=\"row\" className=\"px-5 py-4 font-normal\">\n                      <span className=\"block font-medium\">{e.label}</span>\n                      <span className=\"text-muted-foreground block text-xs\">{e.description}</span>\n                    </th>\n                    {channels.map((c) => (\n                      <td key={c.id} className=\"px-3 py-4 text-center\">\n                        <Switch aria-label={`${e.label} by ${c.label}`} checked={values.events[e.id]?.[c.id] ?? false} onCheckedChange={(on) => toggle(e.id, c.id, on)} />\n                      </td>\n                    ))}\n                  </tr>\n                ))}\n              </tbody>\n            </table>\n          </div>\n\n          {/* Phones: one card per event with its three switches. */}\n          <ul className=\"mt-4 space-y-3 sm:hidden\">\n            {events.map((e) => (\n              <li key={e.id} className=\"bg-card rounded-2xl border p-4\">\n                <p className=\"text-sm font-medium\">{e.label}</p>\n                <p className=\"text-muted-foreground text-xs\">{e.description}</p>\n                <div className=\"mt-3 grid grid-cols-3 gap-2\">\n                  {channels.map((c) => (\n                    <label key={c.id} className=\"flex flex-col items-center gap-2 rounded-xl border py-2.5 text-xs\">\n                      {c.label}\n                      <Switch aria-label={`${e.label} by ${c.label}`} checked={values.events[e.id]?.[c.id] ?? false} onCheckedChange={(on) => toggle(e.id, c.id, on)} />\n                    </label>\n                  ))}\n                </div>\n              </li>\n            ))}\n          </ul>\n        </div>\n\n        <div className=\"bg-card rounded-2xl border p-5 sm:p-6\">\n          <div className=\"flex items-start justify-between gap-4\">\n            <div>\n              <h3 className=\"flex items-center gap-2 text-sm font-semibold\"><Moon className=\"size-4\" aria-hidden=\"true\" />Quiet hours</h3>\n              <p id={`${quietId}-d`} className=\"text-muted-foreground mt-1 text-sm\">Pause push notifications at night and on days off.</p>\n            </div>\n            <Switch aria-label=\"Quiet hours\" aria-describedby={`${quietId}-d`} checked={values.quiet.enabled} onCheckedChange={(enabled) => commit({ ...values, quiet: { ...values.quiet, enabled } })} />\n          </div>\n          <div className={cn(\"mt-5 grid gap-5 transition-opacity duration-200 motion-reduce:transition-none\", !values.quiet.enabled && \"pointer-events-none opacity-50\")} aria-disabled={!values.quiet.enabled}>\n            <div className=\"grid max-w-md grid-cols-2 gap-4\">\n              <label className=\"grid gap-1.5 text-sm font-medium\">From<Input type=\"time\" disabled={!values.quiet.enabled} value={values.quiet.from} onChange={(e) => commit({ ...values, quiet: { ...values.quiet, from: e.target.value } })} /></label>\n              <label className=\"grid gap-1.5 text-sm font-medium\">Until<Input type=\"time\" disabled={!values.quiet.enabled} value={values.quiet.to} onChange={(e) => commit({ ...values, quiet: { ...values.quiet, to: e.target.value } })} /></label>\n            </div>\n            <div>\n              <p className=\"mb-2 text-sm font-medium\">On these days</p>\n              <ToggleGroup type=\"multiple\" variant=\"outline\" aria-label=\"Days with quiet hours\" disabled={!values.quiet.enabled} value={values.quiet.days} onValueChange={(d) => commit({ ...values, quiet: { ...values.quiet, days: d } })}>\n                {days.map(([id, short, long]) => (\n                  <ToggleGroupItem key={id} value={id} aria-label={long} className=\"w-9 px-0\">{short}</ToggleGroupItem>\n                ))}\n              </ToggleGroup>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  )\n}\n\nexport { Settings2, type Settings2Props, type Settings2Values, type Settings2Event, type Settings2Channel }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/settings-2/settings-2.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "settings",
      "notifications",
      "preferences",
      "switches",
      "quiet hours",
      "digest"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "Notification preferences with instant saving. Pass events=[{ id, label, description }], defaultValues and onChange(values) (return a promise; throw to show an error).",
      "whenToUse": [
        "Notification, alert and email preference pages",
        "Settings that apply the moment they are toggled"
      ],
      "whenNotToUse": [
        "Forms that need an explicit Save (use settings-1)"
      ],
      "composesWith": [
        "app-shell-1",
        "settings-1",
        "settings-3"
      ],
      "a11y": [
        {
          "keys": "Arrow keys / Space",
          "action": "Digest radios move and select; switches toggle with Space"
        },
        {
          "keys": "Screen reader",
          "action": "Each switch is named 'Event by Channel'; the saving state is announced politely"
        },
        {
          "keys": "Weekdays",
          "action": "A toggle group with full day names as labels"
        }
      ],
      "customization": [
        "events: { id, label, description }[]",
        "defaultValues: digest, events, quiet",
        "onChange(values) async"
      ]
    },
    "examples": [
      "settings-2-demo",
      "settings-2-custom"
    ],
    "url": "https://ui.ballmac.com/components/settings-2"
  },
  "categories": [
    "blocks",
    "settings"
  ],
  "type": "registry:block"
}