{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "claims-1",
  "title": "Claims 1: claim tracker",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An insurance claim tracker: each claim and its stage in words, a line of five stages that stops at the decision when a claim is declined, and the things still needed from the customer with a place to answer each.",
  "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"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/claims-1/claims-1.tsx",
      "content": "// Ballmac UI: Claims 1. https://ui.ballmac.com/blocks/claims-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, CircleAlert, FileText, Loader2 } 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 { cn } from \"@/lib/utils\"\n\nconst STAGES = [\"Received\", \"In review\", \"Assessed\", \"Decided\", \"Paid\"] as const\ntype Claims1Stage = (typeof STAGES)[number]\n\ntype Claims1Request = { id: string; text: string; /** Set once the customer has answered. */ answer?: string }\ntype Claims1Claim = {\n  id: string\n  /** For example \"CLM-20418\". */\n  number: string\n  title: string\n  stage: Claims1Stage\n  /** Set when a claim was turned down. The stages after \"Decided\" are then not reached. */\n  declined?: boolean\n  amount: number\n  updated: string\n  /** What the insurer needs from the customer before it can go on. */\n  requests?: Claims1Request[]\n}\n\ntype Claims1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    claims?: Claims1Claim[]\n    title?: string\n    currency?: string\n    /** Called with the answer to a request. Reject and the request stays open with the message. */\n    onAnswer?: (claimId: string, requestId: string, answer: string) => void | Promise<void>\n  }\n\nconst claims: Claims1Claim[] = [\n  { id: \"c1\", number: \"CLM-20418\", title: \"Water damage in the kitchen\", stage: \"In review\", amount: 4200, updated: \"Today, 09:12\", requests: [{ id: \"r1\", text: \"A photo of the burst pipe and the damaged cabinets.\" }, { id: \"r2\", text: \"The plumber's invoice or quote.\", answer: \"Uploaded invoice from Hale Plumbing, 14 Oct.\" }] },\n  { id: \"c2\", number: \"CLM-19877\", title: \"Cracked windscreen\", stage: \"Paid\", amount: 380, updated: \"6 Oct\" },\n  { id: \"c3\", number: \"CLM-19302\", title: \"Stolen bicycle\", stage: \"Decided\", declined: true, amount: 1100, updated: \"28 Sep\" },\n]\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]\">\n      <div className=\"grid gap-2.5\">{[0, 1, 2].map((i) => <Skeleton key={i} className=\"h-20 rounded-xl\" />)}</div>\n      <Skeleton className=\"h-96 rounded-2xl\" />\n    </div>\n  )\n}\n\n/**\n * An insurance claim tracker: each claim and where it is, a clear line of stages, and the things still needed from the customer with a place to answer.\n * A declined claim stops its line at the decision and says so; stages are written as words, never only drawn.\n */\nfunction Claims1({ claims: list = claims, title = \"Your claims\", currency = \"USD\", onAnswer, state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Claims1Props) {\n  const id = React.useId()\n  const [sel, setSel] = React.useState(list[0]?.id ?? \"\")\n  const [answers, setAnswers] = React.useState<Record<string, string>>({})\n  const [done, setDone] = React.useState<Record<string, string>>({})\n  const [busy, setBusy] = React.useState<string | null>(null)\n  const [err, setErr] = React.useState<{ key: string; text: string } | null>(null)\n  const view = state === \"ready\" && list.length === 0 ? \"empty\" : state\n  const money = (n: number) => new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency, maximumFractionDigits: 0 }).format(n)\n  const claim = list.find((c) => c.id === sel) ?? list[0]\n  const open = (c: Claims1Claim) => (c.requests ?? []).filter((r) => !r.answer && !done[`${c.id}:${r.id}`]).length\n\n  async function send(c: Claims1Claim, r: Claims1Request) {\n    const key = `${c.id}:${r.id}`\n    const text = (answers[key] ?? \"\").trim()\n    if (!text) { setErr({ key, text: \"Write your answer first.\" }); return }\n    setBusy(key)\n    setErr(null)\n    try {\n      await onAnswer?.(c.id, r.id, text)\n      setDone((d) => ({ ...d, [key]: text }))\n    } catch (e) {\n      setErr({ key, text: e instanceof Error ? e.message : \"Your answer was not sent. It is still here.\" })\n    } finally {\n      setBusy(null)\n    }\n  }\n\n  return (\n    <section data-slot=\"claims-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-5xl px-4 py-10 sm:px-6\", className)} {...frameProps(props)}>\n      <h1 id={`${id}-title`} className=\"mb-8 text-3xl font-semibold tracking-[-0.035em] text-balance\">{title}</h1>\n      <StateView state={view} onRetry={onRetry} label=\"Loading your claims\" loading={<Skel />} empty={{ title: \"No claims yet\", description: \"When you make a claim, you can follow it here.\", ...emptyState }} error={{ title: \"Your claims could not be loaded\", description: \"Nothing was changed. Check your connection and try again.\", ...errorState }}>\n        {claim && (\n          <div className=\"grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]\">\n            <ul aria-label=\"Claims\" className=\"grid content-start gap-2.5\">\n              {list.map((c) => {\n                const n = open(c)\n                return (\n                  <li key={c.id}>\n                    <button type=\"button\" aria-pressed={c.id === claim.id} onClick={() => { setSel(c.id); setErr(null) }} className={cn(\"bg-card focus-visible:ring-ring/50 w-full rounded-xl border p-3.5 text-start outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none\", c.id === claim.id ? \"border-foreground bg-accent/40\" : \"hover:bg-accent/30\")}>\n                      <span className=\"text-muted-foreground block font-mono text-xs\">{c.number}</span>\n                      <span className=\"mt-0.5 block text-sm font-medium\">{c.title}</span>\n                      <span className=\"text-muted-foreground mt-1 flex flex-wrap items-center gap-x-2 text-xs\">\n                        <span>{c.declined ? \"Declined\" : c.stage}</span>\n                        {n > 0 && <span className=\"text-foreground inline-flex items-center gap-1 font-medium\"><CircleAlert aria-hidden=\"true\" className=\"size-3\" />{n} {n === 1 ? \"thing\" : \"things\"} needed</span>}\n                      </span>\n                    </button>\n                  </li>\n                )\n              })}\n            </ul>\n\n            <article aria-labelledby={`${id}-claim`} className=\"bg-card min-w-0 rounded-2xl border p-5 sm:p-6\">\n              <p className=\"text-muted-foreground font-mono text-xs\">{claim.number}</p>\n              <h2 id={`${id}-claim`} className=\"mt-1 text-xl font-semibold tracking-tight\">{claim.title}</h2>\n              <p className=\"text-muted-foreground mt-1 text-sm tabular-nums\">Claimed {money(claim.amount)} · updated {claim.updated}</p>\n\n              <ol aria-label=\"Progress\" className=\"mt-6 grid grid-cols-5 gap-1.5\">\n                {STAGES.map((s, i) => {\n                  const at = STAGES.indexOf(claim.stage)\n                  const reached = i <= at\n                  const lost = claim.declined && i === at\n                  const cut = claim.declined && i > at\n                  return (\n                    <li key={s} aria-current={i === at ? \"step\" : undefined} className=\"min-w-0\">\n                      <span aria-hidden=\"true\" className={cn(\"block h-1.5 rounded-full\", cut ? \"bg-muted/60\" : lost ? \"bg-destructive\" : reached ? \"bg-foreground\" : \"bg-muted\")} />\n                      <span className={cn(\"mt-2 block text-xs leading-tight\", i === at ? \"font-semibold\" : \"text-muted-foreground\")}>\n                        {lost ? \"Declined\" : s}\n                        <span className=\"sr-only\">{cut ? \" (not reached)\" : reached && i < at ? \" (done)\" : i === at ? \" (now)\" : \" (to come)\"}</span>\n                      </span>\n                    </li>\n                  )\n                })}\n              </ol>\n              {claim.declined && <p className=\"bg-muted mt-4 rounded-lg p-3 text-sm\">This claim was declined. If you disagree, reply to the decision letter within 30 days.</p>}\n\n              <h3 className=\"mt-8 mb-3 text-sm font-semibold\">What we need from you</h3>\n              {(claim.requests ?? []).length === 0 ? (\n                <p className=\"text-muted-foreground flex items-center gap-2 text-sm\"><Check aria-hidden=\"true\" className=\"size-4\" />Nothing needed from you right now.</p>\n              ) : (\n                <ul className=\"grid gap-3\">\n                  {(claim.requests ?? []).map((r) => {\n                    const key = `${claim.id}:${r.id}`\n                    const answer = r.answer ?? done[key]\n                    return (\n                      <li key={r.id} className=\"rounded-xl border p-3.5\">\n                        <p className=\"flex items-start gap-2 text-sm font-medium\"><FileText aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0\" />{r.text}</p>\n                        {answer ? (\n                          <p className=\"text-muted-foreground mt-2 flex items-start gap-2 ps-6 text-sm\"><Check aria-hidden=\"true\" className=\"mt-0.5 size-3.5 shrink-0\" /><span><span className=\"sr-only\">Your answer: </span>{answer}</span></p>\n                        ) : (\n                          <div className=\"mt-3 ps-6\">\n                            <label htmlFor={`${id}-${key}`} className=\"sr-only\">Your answer to: {r.text}</label>\n                            <textarea id={`${id}-${key}`} rows={2} value={answers[key] ?? \"\"} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} aria-invalid={err?.key === key} aria-describedby={err?.key === key ? `${id}-${key}-err` : undefined} placeholder=\"Say what you are sending, or paste a link\" className=\"bg-background focus-visible:ring-ring/50 aria-[invalid=true]:border-destructive w-full resize-y rounded-lg border px-3 py-2 text-sm outline-none focus-visible:ring-[3px]\" />\n                            <div className=\"mt-2 flex flex-wrap items-center gap-3\">\n                              <Button size=\"sm\" disabled={busy === key} onClick={() => void send(claim, r)}>{busy === key && <Loader2 aria-hidden=\"true\" className=\"animate-spin\" />} Send answer</Button>\n                              {err?.key === key && <p id={`${id}-${key}-err`} role=\"alert\" className=\"text-destructive text-sm font-medium\">{err.text}</p>}\n                            </div>\n                          </div>\n                        )}\n                      </li>\n                    )\n                  })}\n                </ul>\n              )}\n            </article>\n          </div>\n        )}\n      </StateView>\n    </section>\n  )\n}\n\nexport { Claims1, STAGES as CLAIM_STAGES, type Claims1Claim, type Claims1Props, type Claims1Request, type Claims1Stage }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/claims-1/claims-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "insurance",
      "claims",
      "status",
      "tracker",
      "customer portal",
      "documents",
      "case",
      "finance"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "Claim tracker. <Claims1 claims={[{ id, number, title, stage: 'Received'|'In review'|'Assessed'|'Decided'|'Paid', declined?, amount, updated, requests?: [{ id, text, answer? }] }]} currency='USD' onAnswer={(claimId, requestId, answer) => Promise} />. A declined claim stops at Decided and later stages are written as not reached.",
      "whenToUse": [
        "A customer portal for insurance, warranty or returns",
        "Any case with stages and requests for information"
      ],
      "whenNotToUse": [
        "An internal case queue; use tickets-1",
        "A single order's tracking; use status-1"
      ],
      "composesWith": [
        "tickets-1",
        "upload-1",
        "help-1"
      ],
      "a11y": [
        {
          "keys": "Claims",
          "action": "A list of buttons with pressed state; things needed are written in each"
        },
        {
          "keys": "Stages",
          "action": "An ordered list with the current step marked and each stage's state written for screen readers"
        },
        {
          "keys": "Answer",
          "action": "A labelled text area per request; empty and failed sends are alerts tied to it"
        },
        {
          "keys": "Declined",
          "action": "Said in words in the list and above the stages"
        }
      ],
      "customization": [
        "claims and currency",
        "onAnswer",
        "Stages are fixed: Received, In review, Assessed, Decided, Paid"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "claims-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton list and detail under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No claims yet",
        "example": "claims-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Says no claims yet; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "claims-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was changed, with Try again (onRetry). An answer that fails to send stays in the box and says why."
      }
    ],
    "examples": [
      "claims-1-demo",
      "claims-1-clear",
      "claims-1-loading",
      "claims-1-empty",
      "claims-1-error"
    ],
    "url": "https://ui.ballmac.com/components/claims-1"
  },
  "categories": [
    "blocks",
    "claims"
  ],
  "type": "registry:block"
}