{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "incident-1",
  "title": "Incident 1: incident command",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The page for people fixing an incident: severity and status in words, the impact, who holds commander, comms and ops, a form that posts an update with its new status, updates newest first, and a Resolve button that asks first.",
  "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/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/return-focus.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/incident-1/incident-1.tsx",
      "content": "// Ballmac UI: Incident 1. https://ui.ballmac.com/blocks/incident-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { CircleCheck, Loader2, Megaphone, Send, Siren } 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 { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { useReturnFocus } from \"@/lib/ballmac/return-focus\"\nimport { cn } from \"@/lib/utils\"\n\ntype Incident1Status = \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\"\ntype Incident1Severity = \"SEV1\" | \"SEV2\" | \"SEV3\"\n\ntype Incident1Update = {\n  id: string\n  /** When it was posted, already formatted, such as \"14:31\". */\n  time: string\n  author: string\n  status: Incident1Status\n  body: string\n}\n\ntype Incident1Data = {\n  /** For example \"INC-2041\". */\n  id: string\n  title: string\n  severity: Incident1Severity\n  status: Incident1Status\n  /** When it started, such as \"14:02\". */\n  startedAt: string\n  /** How long it has run, already formatted, such as \"47 min\". */\n  duration: string\n  /** Who or what is affected, in a sentence. */\n  impact: string\n  roles: { commander?: string; comms?: string; ops?: string }\n  updates: Incident1Update[]\n}\n\ntype Incident1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    incident?: Incident1Data | null\n    /** People who can take a role. */\n    people?: string[]\n    /** The person writing updates. */\n    me?: string\n    /** Called when an update is posted. Reject and the message says it was not posted. */\n    onPostUpdate?: (update: { status: Incident1Status; body: string }) => void | Promise<void>\n    /** Called when the incident is resolved after the confirmation. */\n    onResolve?: () => void | Promise<void>\n    /** Called when a role changes. */\n    onAssign?: (role: \"commander\" | \"comms\" | \"ops\", person: string) => void\n  }\n\nconst STATUS: Record<Incident1Status, string> = { investigating: \"Investigating\", identified: \"Identified\", monitoring: \"Monitoring\", resolved: \"Resolved\" }\nconst ROLES: { id: \"commander\" | \"comms\" | \"ops\"; label: string; hint: string }[] = [\n  { id: \"commander\", label: \"Incident commander\", hint: \"Makes the calls\" },\n  { id: \"comms\", label: \"Communications\", hint: \"Updates customers\" },\n  { id: \"ops\", label: \"Operations\", hint: \"Fixes it\" },\n]\n\nconst sample: Incident1Data = {\n  id: \"INC-2041\",\n  title: \"Checkout is failing for some customers\",\n  severity: \"SEV2\",\n  status: \"identified\",\n  startedAt: \"14:02\",\n  duration: \"47 min\",\n  impact: \"About one in five card payments fail. Other payment methods and the rest of the app work.\",\n  roles: { commander: \"Priya Shah\", ops: \"Theo Nakamura\" },\n  updates: [\n    { id: \"u3\", time: \"14:41\", author: \"Theo Nakamura\", status: \"identified\", body: \"The payment provider's EU endpoint is timing out. We are moving traffic to the backup endpoint.\" },\n    { id: \"u2\", time: \"14:20\", author: \"Priya Shah\", status: \"investigating\", body: \"Failures started at 14:02 and only affect cards. We have ruled out the last deploy.\" },\n    { id: \"u1\", time: \"14:08\", author: \"Priya Shah\", status: \"investigating\", body: \"We are looking into reports of failed card payments.\" },\n  ],\n}\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-4\">\n      <Skeleton className=\"h-40 rounded-xl\" />\n      <Skeleton className=\"h-24 rounded-xl\" />\n      <Skeleton className=\"h-56 rounded-xl\" />\n    </div>\n  )\n}\n\n/**\n * The page the people fixing an incident share: severity and status in words, what is affected, who holds the three roles, a form to post an update with\n * its new status, the updates newest first, and a Resolve button that asks first. Severity is never colour alone.\n */\nfunction Incident1({ incident: incidentProp = sample, people = [\"Priya Shah\", \"Theo Nakamura\", \"Noor Haddad\", \"Kofi Mensah\", \"Ana Souza\"], me = \"Priya Shah\", onPostUpdate, onResolve, onAssign, state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Incident1Props) {\n  const id = React.useId()\n  const [incident, setIncident] = React.useState<Incident1Data | null>(incidentProp)\n  React.useEffect(() => setIncident(incidentProp), [incidentProp])\n  const [status, setStatus] = React.useState<Incident1Status>(incidentProp?.status === \"resolved\" ? \"resolved\" : (incidentProp?.status ?? \"investigating\"))\n  const [body, setBody] = React.useState(\"\")\n  const [busy, setBusy] = React.useState(false)\n  const [error, setError] = React.useState<string | null>(null)\n  const [confirm, setConfirm] = React.useState(false)\n  const ret = useReturnFocus(confirm)\n  const view = state === \"ready\" && !incident ? \"empty\" : state\n  const resolved = incident?.status === \"resolved\"\n\n  async function post(next: { status: Incident1Status; body: string }) {\n    if (!incident) return false\n    setBusy(true)\n    setError(null)\n    try {\n      await onPostUpdate?.(next)\n      const update: Incident1Update = { id: `u${incident.updates.length + 1}-${next.body.length}`, time: \"now\", author: me, ...next }\n      setIncident((i) => (i ? { ...i, status: next.status, updates: [update, ...i.updates] } : i))\n      return true\n    } catch (e) {\n      setError(e instanceof Error ? e.message : \"The update was not posted. Your text is still here.\")\n      return false\n    } finally {\n      setBusy(false)\n    }\n  }\n  async function submit(e: React.FormEvent) {\n    e.preventDefault()\n    if (!body.trim()) return\n    if (await post({ status, body: body.trim() })) setBody(\"\")\n  }\n  async function resolve() {\n    try {\n      await onResolve?.()\n      if (await post({ status: \"resolved\", body: \"This incident is resolved.\" })) setStatus(\"resolved\")\n    } catch (e) {\n      setError(e instanceof Error ? e.message : \"The incident could not be resolved. It is still open.\")\n    }\n  }\n\n  return (\n    <section data-slot=\"incident-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      <StateView state={view} onRetry={onRetry} label=\"Loading the incident\" loading={<Skel />} empty={{ title: \"No incident right now\", description: \"When something breaks, the incident page opens here with its roles and updates.\", icon: <CircleCheck />, ...emptyState }} error={{ title: \"The incident could not be loaded\", description: \"Nothing was posted. Check your connection and try again.\", ...errorState }}>\n        {incident && (\n          <>\n            <header className=\"bg-card rounded-xl border p-5 sm:p-6\">\n              <p className=\"flex flex-wrap items-center gap-x-3 gap-y-2 text-sm\">\n                <span className=\"text-muted-foreground font-mono text-xs\" dir=\"ltr\">{incident.id}</span>\n                <span className={cn(\"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-semibold\", incident.severity === \"SEV1\" ? \"border-destructive text-destructive\" : incident.severity === \"SEV2\" ? \"border-chart-3 text-foreground\" : \"text-muted-foreground\")}>\n                  <Siren aria-hidden=\"true\" className=\"size-3.5\" />\n                  <span className=\"sr-only\">Severity </span>\n                  {incident.severity}\n                </span>\n                <span className=\"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium\">\n                  {resolved ? <CircleCheck aria-hidden=\"true\" className=\"size-3.5\" /> : <Megaphone aria-hidden=\"true\" className=\"size-3.5\" />}\n                  {STATUS[incident.status]}\n                </span>\n                <span className=\"text-muted-foreground text-xs tabular-nums\">Started {incident.startedAt} · {resolved ? \"lasted\" : \"running for\"} {incident.duration}</span>\n              </p>\n              <h1 id={`${id}-title`} className=\"mt-3 text-2xl font-semibold tracking-[-0.03em] text-balance sm:text-3xl\">{incident.title}</h1>\n              <p className=\"text-muted-foreground mt-2 max-w-2xl text-pretty\"><span className=\"text-foreground font-medium\">Impact: </span>{incident.impact}</p>\n            </header>\n\n            <div className=\"mt-4 grid gap-3 sm:grid-cols-3\">\n              {ROLES.map((r) => (\n                <div key={r.id} className=\"bg-card rounded-xl border p-4\">\n                  <label htmlFor={`${id}-${r.id}`} className=\"text-sm font-medium\">{r.label}</label>\n                  <p className=\"text-muted-foreground text-xs\">{r.hint}</p>\n                  <select id={`${id}-${r.id}`} disabled={resolved} value={incident.roles[r.id] ?? \"\"} onChange={(e) => { setIncident((i) => (i ? { ...i, roles: { ...i.roles, [r.id]: e.target.value } } : i)); onAssign?.(r.id, e.target.value) }} className=\"bg-background focus-visible:ring-ring/50 mt-2.5 h-9 w-full rounded-md border px-2 text-sm outline-none focus-visible:ring-[3px] disabled:opacity-60\">\n                    <option value=\"\">Unassigned</option>\n                    {people.map((p) => (\n                      <option key={p}>{p}</option>\n                    ))}\n                  </select>\n                </div>\n              ))}\n            </div>\n\n            {!resolved && (\n              <form onSubmit={submit} className=\"bg-card mt-4 grid gap-3 rounded-xl border p-4 sm:p-5\">\n                <label htmlFor={`${id}-body`} className=\"text-sm font-medium\">Post an update</label>\n                <textarea id={`${id}-body`} value={body} onChange={(e) => setBody(e.target.value)} rows={3} placeholder=\"What is known, what is being done, and when the next update is\" className=\"bg-background focus-visible:ring-ring/50 min-h-20 w-full resize-y rounded-lg border px-3 py-2 text-sm outline-none focus-visible:ring-[3px]\" />\n                <div className=\"flex flex-wrap items-center justify-between gap-3\">\n                  <label className=\"flex items-center gap-2 text-sm\">\n                    <span className=\"text-muted-foreground\">Status</span>\n                    <select value={status} onChange={(e) => setStatus(e.target.value as Incident1Status)} className=\"bg-background focus-visible:ring-ring/50 h-9 rounded-md border px-2 text-sm outline-none focus-visible:ring-[3px]\">\n                      {([\"investigating\", \"identified\", \"monitoring\"] as const).map((s) => (\n                        <option key={s} value={s}>{STATUS[s]}</option>\n                      ))}\n                    </select>\n                  </label>\n                  <div className=\"flex gap-2\">\n                    <Button type=\"button\" variant=\"outline\" onClick={() => setConfirm(true)}>Resolve</Button>\n                    <Button type=\"submit\" disabled={!body.trim() || busy}>\n                      {busy ? <Loader2 aria-hidden=\"true\" className=\"animate-spin\" /> : <Send aria-hidden=\"true\" className=\"rtl:-scale-x-100\" />} Post update\n                    </Button>\n                  </div>\n                </div>\n                {error && <p role=\"alert\" className=\"text-destructive text-sm font-medium\">{error}</p>}\n              </form>\n            )}\n            {resolved && <p role=\"status\" className=\"bg-card mt-4 flex items-center gap-2 rounded-xl border px-4 py-3 text-sm\"><CircleCheck aria-hidden=\"true\" className=\"size-4\" /> This incident is resolved. Updates are closed.</p>}\n\n            <ol aria-label=\"Updates, newest first\" className=\"mt-6 grid gap-0\">\n              {incident.updates.map((u, i) => (\n                <li key={u.id} className=\"relative grid grid-cols-[4.5rem_1fr] gap-4 pb-6 last:pb-0\">\n                  <time className=\"text-muted-foreground pt-0.5 text-end font-mono text-xs tabular-nums\" dir=\"ltr\">{u.time}</time>\n                  <div className=\"border-border relative border-s ps-5\">\n                    <span aria-hidden=\"true\" className={cn(\"bg-background absolute top-1 -start-[5px] size-2.5 rounded-full border-2\", i === 0 ? \"border-foreground\" : \"border-border\")} />\n                    <p className=\"text-sm font-medium\">{STATUS[u.status]} <span className=\"text-muted-foreground font-normal\">· {u.author}</span></p>\n                    <p className=\"text-muted-foreground mt-1 text-sm leading-relaxed text-pretty\">{u.body}</p>\n                  </div>\n                </li>\n              ))}\n            </ol>\n          </>\n        )}\n      </StateView>\n\n      <AlertDialog open={confirm} onOpenChange={setConfirm}>\n        <AlertDialogContent {...ret}>\n          <AlertDialogHeader>\n            <AlertDialogTitle>Resolve this incident?</AlertDialogTitle>\n            <AlertDialogDescription>Customers see it as resolved and updates close. Make sure the fix has held.</AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>Keep it open</AlertDialogCancel>\n            <AlertDialogAction onClick={() => void resolve()}>Resolve</AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </section>\n  )\n}\n\nexport { Incident1, type Incident1Props, type Incident1Data, type Incident1Update, type Incident1Status, type Incident1Severity }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/incident-1/incident-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "incident",
      "outage",
      "sre",
      "status",
      "severity",
      "postmortem",
      "on-call",
      "devops"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "An incident command page. <Incident1 incident={{ id, title, severity: 'SEV1' | 'SEV2' | 'SEV3', status: 'investigating' | 'identified' | 'monitoring' | 'resolved', startedAt, duration, impact, roles: { commander?, comms?, ops? }, updates: [{ id, time, author, status, body }] }} people={[...]} me='Priya Shah' onPostUpdate={({ status, body }) => Promise} onResolve={() => Promise} onAssign={(role, person) => void} />. Pass incident={null} for no incident. Resolve asks first, posts a final update and closes the form.",
      "whenToUse": [
        "The shared page during an outage",
        "A status workflow where updates and roles are tracked in one place"
      ],
      "whenNotToUse": [
        "A public status page for customers; use status-1",
        "A list of past incidents; use a table or tickets-1"
      ],
      "composesWith": [
        "status-1",
        "oncall-1",
        "audit-1",
        "app-shell-1"
      ],
      "a11y": [
        {
          "keys": "Severity and status",
          "action": "Written as words with icons (SEV2, Identified), never colour alone; the severity is prefixed 'Severity' for screen readers"
        },
        {
          "keys": "Roles",
          "action": "Three labelled native selects"
        },
        {
          "keys": "Post an update",
          "action": "A labelled textarea and select; Post update is off until there is text; an error is an alert and the text stays"
        },
        {
          "keys": "Resolve",
          "action": "An alert dialog that returns focus to the button when closed"
        },
        {
          "keys": "Updates",
          "action": "An ordered list named 'Updates, newest first'; times stay left to right"
        }
      ],
      "customization": [
        "people and me",
        "onPostUpdate, onResolve and onAssign",
        "incident={null} shows the empty state"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "incident-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton for the header, roles and timeline under the page, announced as loading."
      },
      {
        "id": "empty",
        "title": "No incident right now",
        "example": "incident-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Says there is no incident right now; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "incident-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was posted, with Try again (onRetry). A failed update keeps your text and says why."
      }
    ],
    "examples": [
      "incident-1-demo",
      "incident-1-resolved",
      "incident-1-loading",
      "incident-1-empty",
      "incident-1-error"
    ],
    "url": "https://ui.ballmac.com/components/incident-1"
  },
  "categories": [
    "blocks",
    "incident"
  ],
  "type": "registry:block"
}