{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "alerts-1",
  "title": "Alerts 1: alert rules",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The alert rules of a monitoring setup: what each watches, severity, where it notifies, and firing, muted or fine in words, firing first, switch off, mute for an hour, four hours or a day, send a test, all undone with the reason if it fails.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.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/alerts-1/alerts-1.tsx",
      "content": "// Ballmac UI: Alerts 1. https://ui.ballmac.com/blocks/alerts-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { BellOff, BellRing, CircleAlert, Info, Loader2, Send, TriangleAlert, type LucideIcon } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\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 Alerts1Severity = \"critical\" | \"warning\" | \"info\"\ntype Alerts1Status = \"firing\" | \"ok\" | \"muted\"\n\ntype Alerts1Rule = {\n  id: string\n  name: string\n  /** The condition, as code, for example \"error_rate > 2% for 5m\". */\n  condition: string\n  severity: Alerts1Severity\n  /** Where it notifies: \"Pager\", \"Slack #oncall\", \"Email\". */\n  channel: string\n  status: Alerts1Status\n  enabled: boolean\n  /** For a firing rule, since when, already formatted. */\n  since?: string\n  /** For a muted rule, until when, already formatted. */\n  until?: string\n}\n\ntype Alerts1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    rules?: Alerts1Rule[]\n    title?: string\n    /** Called when a rule is switched on or off. Reject and it goes back with the message. */\n    onToggle?: (rule: Alerts1Rule, enabled: boolean) => void | Promise<void>\n    /** Called to mute a rule for a while, or unmute it when `hours` is 0. */\n    onMute?: (rule: Alerts1Rule, hours: number) => void | Promise<void>\n    /** Called to send a test notification to the rule's channel. */\n    onTest?: (rule: Alerts1Rule) => void | Promise<void>\n  }\n\nconst sample: Alerts1Rule[] = [\n  { id: \"a1\", name: \"Checkout error rate\", condition: \"checkout.error_rate > 2% for 5m\", severity: \"critical\", channel: \"Pager\", status: \"firing\", enabled: true, since: \"14:02\" },\n  { id: \"a2\", name: \"API latency (p95)\", condition: \"api.latency.p95 > 800ms for 10m\", severity: \"warning\", channel: \"Slack #oncall\", status: \"ok\", enabled: true },\n  { id: \"a3\", name: \"Queue depth: email\", condition: \"queue.email.depth > 5000 for 15m\", severity: \"warning\", channel: \"Slack #platform\", status: \"firing\", enabled: true, since: \"13:40\" },\n  { id: \"a4\", name: \"Disk above 85%\", condition: \"disk.used_percent > 85\", severity: \"warning\", channel: \"Email\", status: \"muted\", enabled: true, until: \"tomorrow 09:00\" },\n  { id: \"a5\", name: \"Certificate expires soon\", condition: \"tls.days_left < 14\", severity: \"info\", channel: \"Email\", status: \"ok\", enabled: true },\n  { id: \"a6\", name: \"Signup drop\", condition: \"signups.rate < 40% of last week\", severity: \"info\", channel: \"Slack #growth\", status: \"ok\", enabled: false },\n]\n\nconst SEV: Record<Alerts1Severity, { icon: LucideIcon; label: string }> = {\n  critical: { icon: CircleAlert, label: \"Critical\" },\n  warning: { icon: TriangleAlert, label: \"Warning\" },\n  info: { icon: Info, label: \"Info\" },\n}\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-3\">\n      {[0, 1, 2, 3, 4].map((i) => (\n        <Skeleton key={i} className=\"h-24 rounded-xl\" />\n      ))}\n    </div>\n  )\n}\n\n/**\n * The alert rules of a monitoring setup: what each watches for, how serious it is, where it notifies and whether it is firing, muted or fine, all in words. Firing\n * rules come first. Each can be switched off, muted for an hour, four hours or a day, and tested, and every change is undone with the reason if it fails.\n */\nfunction Alerts1({ rules: rulesProp = sample, title = \"Alert rules\", onToggle, onMute, onTest, state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Alerts1Props) {\n  const id = React.useId()\n  const [rules, setRules] = React.useState(rulesProp)\n  React.useEffect(() => setRules(rulesProp), [rulesProp])\n  const [filter, setFilter] = React.useState<\"all\" | Alerts1Status | \"off\">(\"all\")\n  const [hours, setHours] = React.useState<Record<string, number>>({})\n  const [busy, setBusy] = React.useState<string | null>(null)\n  const [note, setNote] = React.useState<{ id: string; ok: boolean; text: string } | null>(null)\n  const view = state === \"ready\" && rules.length === 0 ? \"empty\" : state\n  const rank: Record<string, number> = { firing: 0, ok: 1, muted: 2 }\n  const count = (f: typeof filter) => rules.filter((r) => (f === \"all\" ? true : f === \"off\" ? !r.enabled : r.enabled && r.status === f)).length\n  const shown = rules.filter((r) => (filter === \"all\" ? true : filter === \"off\" ? !r.enabled : r.enabled && r.status === filter)).sort((a, b) => (a.enabled === b.enabled ? rank[a.status] - rank[b.status] : a.enabled ? -1 : 1) || [\"critical\", \"warning\", \"info\"].indexOf(a.severity) - [\"critical\", \"warning\", \"info\"].indexOf(b.severity))\n  const firing = rules.filter((r) => r.enabled && r.status === \"firing\")\n\n  async function run(r: Alerts1Rule, work: () => void | Promise<void>, apply: (x: Alerts1Rule) => Alerts1Rule, ok: string) {\n    setBusy(r.id)\n    setNote(null)\n    setRules((l) => l.map((x) => (x.id === r.id ? apply(x) : x)))\n    try {\n      await work()\n      setNote({ id: r.id, ok: true, text: ok })\n    } catch (e) {\n      setRules((l) => l.map((x) => (x.id === r.id ? r : x)))\n      setNote({ id: r.id, ok: false, text: e instanceof Error ? e.message : \"That did not go through, so nothing changed.\" })\n    } finally {\n      setBusy(null)\n    }\n  }\n  const h = (r: Alerts1Rule) => hours[r.id] ?? 1\n\n  return (\n    <section data-slot=\"alerts-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-4xl 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.035em] text-balance\">\n          {title}\n        </h1>\n        <p aria-live=\"polite\" className=\"text-muted-foreground mt-2 max-w-xl text-pretty\">\n          {firing.length ? <><strong className=\"text-foreground\">{firing.length} {firing.length === 1 ? \"rule is\" : \"rules are\"} firing.</strong> They are listed first.</> : \"Nothing is firing.\"}\n        </p>\n      </header>\n      <StateView state={view} onRetry={onRetry} label=\"Loading alert rules\" loading={<Skel />} empty={{ title: \"No alert rules yet\", description: \"A rule watches a measurement and tells someone when it crosses a line.\", ...emptyState }} error={{ title: \"Alert rules could not be loaded\", description: \"Nothing was changed or muted. Check your connection and try again.\", ...errorState }}>\n        <div role=\"group\" aria-label=\"Filter rules\" className=\"mb-4 flex flex-wrap gap-2\">\n          {([\"all\", \"firing\", \"muted\", \"off\"] as const).map((k) => (\n            <button key={k} type=\"button\" aria-pressed={filter === k} onClick={() => setFilter(k)} className={cn(\"focus-visible:ring-ring/50 rounded-full border px-3.5 py-1.5 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none\", filter === k ? \"bg-foreground text-background border-foreground\" : \"hover:bg-accent\")}>\n              {k === \"all\" ? \"All\" : k === \"firing\" ? \"Firing\" : k === \"muted\" ? \"Muted\" : \"Switched off\"} <span className={cn(\"tabular-nums\", filter === k ? \"opacity-70\" : \"text-muted-foreground\")}>{count(k)}</span>\n            </button>\n          ))}\n        </div>\n        {shown.length ? (\n          <ul className=\"grid gap-3\">\n            {shown.map((r) => {\n              const S = SEV[r.severity]\n              const firingNow = r.enabled && r.status === \"firing\"\n              return (\n                <li key={r.id} data-status={r.enabled ? r.status : \"off\"} className={cn(\"bg-card rounded-xl border p-4 sm:p-5\", firingNow && \"border-destructive/60\", !r.enabled && \"bg-muted/30\")}>\n                  <div className=\"flex flex-wrap items-start justify-between gap-x-6 gap-y-3\">\n                    <div className=\"min-w-0\">\n                      <p className=\"flex flex-wrap items-center gap-x-2.5 gap-y-1\">\n                        <span className=\"font-medium\">{r.name}</span>\n                        <span className={cn(\"inline-flex items-center gap-1 rounded-full border px-2 py-px text-[11px]\", r.severity === \"critical\" && \"border-destructive/60 text-destructive\")}><S.icon aria-hidden=\"true\" className=\"size-3\" />{S.label}</span>\n                      </p>\n                      <code className=\"bg-muted mt-1.5 inline-block rounded px-1.5 py-0.5 font-mono text-xs\" dir=\"ltr\">{r.condition}</code>\n                      <p className=\"text-muted-foreground mt-1.5 flex flex-wrap items-center gap-x-3 text-xs\">\n                        <span>Notifies {r.channel}</span>\n                        {!r.enabled ? <span className=\"text-foreground font-medium\">Switched off</span> : r.status === \"firing\" ? <span className=\"text-foreground flex items-center gap-1 font-medium\"><BellRing aria-hidden=\"true\" className=\"text-destructive size-3.5\" /> Firing{r.since ? ` since ${r.since}` : \"\"}</span> : r.status === \"muted\" ? <span className=\"text-foreground flex items-center gap-1 font-medium\"><BellOff aria-hidden=\"true\" className=\"size-3.5\" /> Muted{r.until ? ` until ${r.until}` : \"\"}</span> : <span>Fine</span>}\n                      </p>\n                    </div>\n                    <Switch aria-label={`${r.name} is on`} checked={r.enabled} disabled={busy === r.id} onCheckedChange={(v) => void run(r, () => onToggle?.(r, v), (x) => ({ ...x, enabled: v }), `${r.name} is ${v ? \"on\" : \"switched off\"}.`)} />\n                  </div>\n                  {r.enabled && (\n                    <div className=\"mt-3 flex flex-wrap items-center gap-2 border-t pt-3\">\n                      {r.status === \"muted\" ? (\n                        <Button size=\"sm\" variant=\"outline\" disabled={busy === r.id} onClick={() => void run(r, () => onMute?.(r, 0), (x) => ({ ...x, status: \"ok\", until: undefined }), `${r.name} is unmuted.`)}><BellRing aria-hidden=\"true\" /> Unmute</Button>\n                      ) : (\n                        <>\n                          <label className=\"flex items-center gap-2 text-sm\">\n                            <span className=\"sr-only\">Mute {r.name} for</span>\n                            <select value={h(r)} onChange={(e) => setHours({ ...hours, [r.id]: Number(e.target.value) })} aria-label={`Mute ${r.name} for`} className=\"bg-background focus-visible:ring-ring/50 h-8 rounded-md border px-2 text-sm outline-none focus-visible:ring-[3px]\">\n                              <option value={1}>1 hour</option>\n                              <option value={4}>4 hours</option>\n                              <option value={24}>1 day</option>\n                            </select>\n                          </label>\n                          <Button size=\"sm\" variant=\"outline\" disabled={busy === r.id} onClick={() => void run(r, () => onMute?.(r, h(r)), (x) => ({ ...x, status: \"muted\", until: `${h(r)} ${h(r) === 1 ? \"hour\" : \"hours\"} from now` }), `${r.name} is muted for ${h(r) === 24 ? \"a day\" : `${h(r)} ${h(r) === 1 ? \"hour\" : \"hours\"}`}.`)} aria-label={`Mute ${r.name}`}><BellOff aria-hidden=\"true\" /> Mute</Button>\n                        </>\n                      )}\n                      <Button size=\"sm\" variant=\"ghost\" disabled={busy === r.id} onClick={() => void run(r, () => onTest?.(r), (x) => x, `A test was sent to ${r.channel}.`)} aria-label={`Send a test for ${r.name}`}>{busy === r.id ? <Loader2 aria-hidden=\"true\" className=\"animate-spin\" /> : <Send aria-hidden=\"true\" className=\"rtl:-scale-x-100\" />} Send test</Button>\n                    </div>\n                  )}\n                  {note?.id === r.id && <p role={note.ok ? \"status\" : \"alert\"} className={cn(\"mt-2 text-sm font-medium\", !note.ok && \"text-destructive\")}>{note.text}</p>}\n                </li>\n              )\n            })}\n          </ul>\n        ) : (\n          <p className=\"text-muted-foreground rounded-xl border border-dashed px-4 py-12 text-center text-sm\">No rules here.</p>\n        )}\n      </StateView>\n    </section>\n  )\n}\n\nexport { Alerts1, type Alerts1Props, type Alerts1Rule, type Alerts1Severity, type Alerts1Status }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/alerts-1/alerts-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "alerts",
      "monitoring",
      "rules",
      "pager",
      "on-call",
      "notifications",
      "observability",
      "sre"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "Alert rules. <Alerts1 rules={[{ id, name, condition, severity: 'critical' | 'warning' | 'info', channel, status: 'firing' | 'ok' | 'muted', enabled, since?, until? }]} onToggle={(rule, enabled) => Promise} onMute={(rule, hours) => Promise /* hours 0 means unmute */} onTest={(rule) => Promise} />. Firing rules are listed first. Each action updates the screen at once and is undone if the handler rejects.",
      "whenToUse": [
        "The settings page of a monitoring or alerting tool",
        "An on-call overview of what is firing and what is muted"
      ],
      "whenNotToUse": [
        "Notification preferences for end users; use settings-1",
        "A feed of past alerts; use audit-1 or log-stream"
      ],
      "composesWith": [
        "oncall-1",
        "incident-1",
        "runbook-1",
        "slo-1"
      ],
      "a11y": [
        {
          "keys": "Status",
          "action": "Firing, Muted until… and Switched off are written as words with icons; severity is written too"
        },
        {
          "keys": "Switch",
          "action": "Named '<rule> is on'; a failure is an alert and the switch goes back"
        },
        {
          "keys": "Mute",
          "action": "A labelled select for how long and a button named for the rule"
        },
        {
          "keys": "Condition",
          "action": "Shown as code that stays left to right"
        },
        {
          "keys": "Summary",
          "action": "The count of firing rules is a polite live region"
        }
      ],
      "customization": [
        "rules and channels",
        "onToggle, onMute and onTest",
        "Mute durations are 1 hour, 4 hours and 1 day"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "alerts-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton rows under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No alert rules yet",
        "example": "alerts-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Says no alert rules yet; emptyState sets the copy. A filter with no match says so."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "alerts-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was changed or muted, with Try again (onRetry). A change that fails goes back and says why."
      }
    ],
    "examples": [
      "alerts-1-demo",
      "alerts-1-quiet",
      "alerts-1-loading",
      "alerts-1-empty",
      "alerts-1-error"
    ],
    "url": "https://ui.ballmac.com/components/alerts-1"
  },
  "categories": [
    "blocks",
    "alerts"
  ],
  "type": "registry:block"
}