{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "matter-1",
  "title": "Matter 1: legal matters",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A law firm's matters: deadlines that cannot be missed first with overdue and due-soon in words, then each matter with client, lead and status, hours against the month's budget, search, a practice area filter and a show-closed switch.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/match-highlight.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/matter-1/matter-1.tsx",
      "content": "// Ballmac UI: Matter 1. https://ui.ballmac.com/blocks/matter-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { CalendarClock, CircleAlert, Search } from \"lucide-react\"\n\nimport { MatchHighlight, matchesAll, queryTokens } from \"@/components/ballmac/match-highlight\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Matter1Matter = {\n  id: string\n  /** The client, for example \"Harlow Logistics Ltd\". */\n  client: string\n  /** The matter, for example \"Lease renewal, Unit 4\". */\n  title: string\n  area: string\n  /** The lawyer in charge. */\n  lead: string\n  status: \"open\" | \"waiting\" | \"closed\"\n  /** The next date that cannot be missed, with what it is. */\n  deadline?: { label: string; date: string; /** Days from today; negative is overdue. */ days: number }\n  /** Hours recorded this month. */\n  hours: number\n  /** Hours budgeted for this month, if the matter has a budget. */\n  budget?: number\n}\n\ntype Matter1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    matters?: Matter1Matter[]\n    title?: string\n    /** Show matters with a deadline within this many days as \"soon\". */\n    soonDays?: number\n  }\n\nconst sample: Matter1Matter[] = [\n  { id: \"m1\", client: \"Harlow Logistics Ltd\", title: \"Lease renewal, Unit 4\", area: \"Property\", lead: \"A. Okafor\", status: \"open\", deadline: { label: \"Reply to landlord's notice\", date: \"Mon 27 Oct\", days: 3 }, hours: 14.5, budget: 20 },\n  { id: \"m2\", client: \"Dr. Elena Marsh\", title: \"Employment tribunal response\", area: \"Employment\", lead: \"J. Weber\", status: \"open\", deadline: { label: \"File the response (ET3)\", date: \"Thu 23 Oct\", days: -1 }, hours: 31, budget: 28 },\n  { id: \"m3\", client: \"Brightwater Foods\", title: \"Supplier contract review\", area: \"Commercial\", lead: \"L. Rossi\", status: \"waiting\", deadline: { label: \"Counterparty's comments due\", date: \"Fri 31 Oct\", days: 7 }, hours: 6.25 },\n  { id: \"m4\", client: \"The Okonkwo family\", title: \"Estate administration\", area: \"Probate\", lead: \"A. Okafor\", status: \"open\", deadline: { label: \"Inheritance tax payment\", date: \"Fri 28 Nov\", days: 35 }, hours: 9, budget: 12 },\n  { id: \"m5\", client: \"Northpoint Dental\", title: \"Data protection policy\", area: \"Commercial\", lead: \"L. Rossi\", status: \"open\", hours: 3.5, budget: 8 },\n  { id: \"m6\", client: \"M. Petrov\", title: \"Boundary dispute\", area: \"Property\", lead: \"J. Weber\", status: \"closed\", hours: 0 },\n]\n\nconst STATUS = { open: \"Open\", waiting: \"Waiting on others\", closed: \"Closed\" } as const\nconst hrs = (n: number) => `${+n.toFixed(2)} h`\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-3\">\n      <Skeleton className=\"h-20 rounded-xl\" />\n      {[0, 1, 2, 3, 4].map((i) => (\n        <Skeleton key={i} className=\"h-16 rounded-lg\" />\n      ))}\n    </div>\n  )\n}\n\n/**\n * A law firm's matters: the dates that cannot be missed come first and say overdue or due soon in words, then the list with client, lead, status and hours\n * against the month's budget. Search and the practice area filter narrow it, and a matter over its budget says so instead of just turning red.\n */\nfunction Matter1({ matters = sample, title = \"Matters\", soonDays = 7, state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Matter1Props) {\n  const id = React.useId()\n  const [query, setQuery] = React.useState(\"\")\n  const [area, setArea] = React.useState(\"all\")\n  const [showClosed, setShowClosed] = React.useState(false)\n  const tokens = queryTokens(query)\n  const view = state === \"ready\" && matters.length === 0 ? \"empty\" : state\n  const areas = [...new Set(matters.map((m) => m.area))]\n  const shown = matters.filter((m) => (showClosed || m.status !== \"closed\") && (area === \"all\" || m.area === area) && (!tokens.length || matchesAll(`${m.client} ${m.title} ${m.lead} ${m.area}`, tokens)))\n  const dated = matters.filter((m) => m.deadline && m.status !== \"closed\").sort((a, b) => a.deadline!.days - b.deadline!.days)\n  const urgent = dated.filter((m) => m.deadline!.days <= soonDays)\n  const when = (days: number) => (days < 0 ? `${Math.abs(days)} ${Math.abs(days) === 1 ? \"day\" : \"days\"} overdue` : days === 0 ? \"Due today\" : `In ${days} ${days === 1 ? \"day\" : \"days\"}`)\n\n  return (\n    <section data-slot=\"matter-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      <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 className=\"text-muted-foreground mt-2 max-w-xl text-pretty\">The dates that cannot be missed first, then every matter and how its hours are going.</p>\n      </header>\n      <StateView state={view} onRetry={onRetry} label=\"Loading matters\" loading={<Skel />} empty={{ title: \"No matters yet\", description: \"Open a matter for a client and it will appear here with its deadlines and hours.\", ...emptyState }} error={{ title: \"Matters could not be loaded\", description: \"Nothing was changed. Check your connection and try again.\", ...errorState }}>\n        <section aria-labelledby={`${id}-dates`} className=\"bg-card rounded-xl border p-4 sm:p-5\">\n          <h2 id={`${id}-dates`} className=\"text-muted-foreground flex items-center gap-2 text-xs font-medium\"><CalendarClock aria-hidden=\"true\" className=\"size-3.5\" /> Dates that cannot be missed</h2>\n          {dated.length ? (\n            <ol className=\"mt-3 divide-y\">\n              {dated.slice(0, 4).map((m) => {\n                const over = m.deadline!.days < 0\n                return (\n                  <li key={m.id} className=\"flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-2.5 text-sm\">\n                    <span className=\"min-w-0\">\n                      <span className=\"font-medium\">{m.deadline!.label}</span>\n                      <span className=\"text-muted-foreground\"> · {m.client}</span>\n                    </span>\n                    <span className={cn(\"flex items-center gap-1.5 tabular-nums\", over ? \"text-destructive font-semibold\" : m.deadline!.days <= soonDays ? \"text-foreground font-medium\" : \"text-muted-foreground\")}>\n                      {(over || m.deadline!.days <= soonDays) && <CircleAlert aria-hidden=\"true\" className=\"size-3.5\" />}\n                      {m.deadline!.date} · {when(m.deadline!.days)}\n                    </span>\n                  </li>\n                )\n              })}\n            </ol>\n          ) : (\n            <p className=\"text-muted-foreground mt-2 text-sm\">No deadlines are set.</p>\n          )}\n          {urgent.length > 0 && <p role=\"status\" className=\"mt-2 text-xs font-medium\">{urgent.length} {urgent.length === 1 ? \"date is\" : \"dates are\"} overdue or due within {soonDays} days.</p>}\n        </section>\n\n        <div className=\"mt-6 flex flex-col gap-3 sm:flex-row sm:items-center\">\n          <div className=\"bg-background focus-within:ring-ring/50 flex h-10 flex-1 items-center gap-2 rounded-lg border px-3 focus-within:ring-[3px]\">\n            <Search aria-hidden=\"true\" className=\"text-muted-foreground size-4\" />\n            <input type=\"search\" aria-label=\"Search matters\" placeholder=\"Search by client, matter, lead or area\" value={query} onChange={(e) => setQuery(e.target.value)} className=\"placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent text-sm outline-none\" />\n          </div>\n          <select aria-label=\"Practice area\" value={area} onChange={(e) => setArea(e.target.value)} className=\"bg-background focus-visible:ring-ring/50 h-10 rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px]\">\n            <option value=\"all\">All areas</option>\n            {areas.map((a) => (\n              <option key={a}>{a}</option>\n            ))}\n          </select>\n          <label className=\"flex items-center gap-2 text-sm\">\n            <input type=\"checkbox\" checked={showClosed} onChange={(e) => setShowClosed(e.target.checked)} className=\"accent-foreground size-4\" /> Show closed\n          </label>\n        </div>\n        <p aria-live=\"polite\" className=\"text-muted-foreground mt-4 text-sm\">{shown.length} {shown.length === 1 ? \"matter\" : \"matters\"}</p>\n\n        {shown.length ? (\n          <div tabIndex={0} role=\"region\" aria-label={`${title}, scrollable`} className=\"mt-3 focus-visible:ring-ring/50 overflow-x-auto rounded-xl border outline-none focus-visible:ring-[3px]\">\n            <table className=\"w-full min-w-[40rem] border-collapse text-sm\">\n              <caption className=\"sr-only\">{title}</caption>\n              <thead className=\"bg-muted/40 text-muted-foreground text-xs\">\n                <tr>\n                  <th scope=\"col\" className=\"px-4 py-2.5 text-start font-medium\">Matter</th>\n                  <th scope=\"col\" className=\"px-3 py-2.5 text-start font-medium\">Lead</th>\n                  <th scope=\"col\" className=\"px-3 py-2.5 text-start font-medium\">Status</th>\n                  <th scope=\"col\" className=\"px-4 py-2.5 text-end font-medium\">Hours this month</th>\n                </tr>\n              </thead>\n              <tbody className=\"divide-y\">\n                {shown.map((m) => {\n                  const over = m.budget !== undefined && m.hours > m.budget\n                  return (\n                    <tr key={m.id} data-status={m.status}>\n                      <th scope=\"row\" className=\"px-4 py-3 text-start font-normal\">\n                        <span className=\"block font-medium\"><MatchHighlight text={m.title} query={tokens} /></span>\n                        <span className=\"text-muted-foreground block text-xs\"><MatchHighlight text={m.client} query={tokens} /> · {m.area}</span>\n                      </th>\n                      <td className=\"text-muted-foreground px-3 py-3\"><MatchHighlight text={m.lead} query={tokens} /></td>\n                      <td className=\"px-3 py-3\">{STATUS[m.status]}</td>\n                      <td className=\"px-4 py-3 text-end tabular-nums\">\n                        <span className=\"font-medium\">{hrs(m.hours)}</span>\n                        {m.budget !== undefined && <span className=\"text-muted-foreground\"> of {hrs(m.budget)}</span>}\n                        {over && <span className=\"text-destructive block text-xs font-medium\">Over budget by {hrs(m.hours - m.budget!)}</span>}\n                      </td>\n                    </tr>\n                  )\n                })}\n              </tbody>\n            </table>\n          </div>\n        ) : (\n          <p className=\"text-muted-foreground mt-3 rounded-xl border border-dashed px-4 py-12 text-center text-sm\">No matters match. Try another search or area.</p>\n        )}\n      </StateView>\n    </section>\n  )\n}\n\nexport { Matter1, type Matter1Matter, type Matter1Props }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/matter-1/matter-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "legal",
      "law firm",
      "matters",
      "deadlines",
      "billable hours",
      "practice management",
      "cases",
      "professional services"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "A matters list. <Matter1 matters={[{ id, client, title, area, lead, status: 'open' | 'waiting' | 'closed', deadline?: { label, date, days /* from today; negative is overdue */ }, hours, budget? }]} soonDays={7} />. The panel on top lists open deadlines soonest first and says 'overdue' or 'in 3 days'; a matter over budget says 'Over budget by 3 h'. Closed matters are hidden until the switch is on.",
      "whenToUse": [
        "A practice-management home for a law firm or any deadline-driven professional service",
        "A list where hours are tracked against a budget"
      ],
      "whenNotToUse": [
        "A general task list; use tasks-1",
        "Time entry; use timesheet-1"
      ],
      "composesWith": [
        "timesheet-1",
        "invoice-1",
        "calendar-1",
        "app-shell-1"
      ],
      "a11y": [
        {
          "keys": "Dates panel",
          "action": "A labelled region with an ordered list; overdue and due-soon are written ('1 day overdue', 'In 3 days') with an icon, never colour alone"
        },
        {
          "keys": "Table",
          "action": "A real table with a caption; each matter is a row header with client and area under it"
        },
        {
          "keys": "Filters",
          "action": "A labelled search, a labelled native select and a checkbox; the count is a polite announcement"
        },
        {
          "keys": "Budget",
          "action": "'Over budget by 3 h' is text under the hours, not only a colour"
        }
      ],
      "customization": [
        "matters, soonDays and the title",
        "closed matters are off by default",
        "Practice areas come from the data"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "matter-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton for the dates panel and rows under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No matters yet",
        "example": "matter-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Says no matters yet; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "matter-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was changed, with Try again (onRetry). A search or filter with no match says so."
      }
    ],
    "examples": [
      "matter-1-demo",
      "matter-1-quiet",
      "matter-1-loading",
      "matter-1-empty",
      "matter-1-error"
    ],
    "url": "https://ui.ballmac.com/components/matter-1"
  },
  "categories": [
    "blocks",
    "matter"
  ],
  "type": "registry:block"
}