{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "status-badge-row",
  "title": "Status Badge Row",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A status page block: an overall banner and one row per service with a status chip, uptime and a 90-day bar history you can explore with the pointer or arrow keys.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json",
    "https://ui.ballmac.com/r/direction.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/status-badge-row.tsx",
      "content": "// Ballmac UI: Status Badge Row. https://ui.ballmac.com/components/status-badge-row\n\"use client\"\n\nimport * as React from \"react\"\nimport { CheckCircle2, CircleAlert, OctagonAlert, Wrench } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useLocale, useMessages, defineMessage, type Message, type Msg } from \"@/lib/ballmac/i18n\"\nimport { useDirection } from \"@/lib/ballmac/direction\"\n\ntype ServiceStatus = \"operational\" | \"degraded\" | \"outage\" | \"maintenance\"\n\ntype ServiceDay = ServiceStatus | { status: ServiceStatus; note?: string }\n\ntype ServiceItem = {\n  /** Service name, such as \"API\". */\n  name: string\n  /** Current status. */\n  status: ServiceStatus\n  /** One line under the name. */\n  description?: string\n  /** Uptime over the history, in percent. Shown with two decimals. */\n  uptime?: number\n  /** One entry per day, oldest first. */\n  days?: ServiceDay[]\n}\n\nconst META: Record<ServiceStatus, { label: Message; icon: React.ComponentType<{ className?: string }>; bar: string; height: string; chip: string; order: number }> = {\n  operational: { label: defineMessage(\"status-badge-row.META.operational\", \"Operational\"), icon: CheckCircle2, bar: \"bg-chart-2\", height: \"100%\", chip: \"border-chart-2/30 bg-chart-2/10\", order: 0 },\n  maintenance: { label: defineMessage(\"status-badge-row.META.maintenance\", \"Maintenance\"), icon: Wrench, bar: \"bg-chart-1\", height: \"82%\", chip: \"border-chart-1/30 bg-chart-1/10\", order: 1 },\n  degraded: { label: defineMessage(\"status-badge-row.META.degraded\", \"Degraded performance\"), icon: CircleAlert, bar: \"bg-chart-3\", height: \"66%\", chip: \"border-chart-3/30 bg-chart-3/10\", order: 2 },\n  outage: { label: defineMessage(\"status-badge-row.META.outage\", \"Outage\"), icon: OctagonAlert, bar: \"bg-destructive\", height: \"44%\", chip: \"border-destructive/30 bg-destructive/10\", order: 3 },\n}\n\n\n/** \"2026-09-30\" minus `back` days, as \"Sep 21\". UTC, so the server and browser agree. */\nfunction dayLabel(locale: string, msg: Msg, endDate: string | undefined, back: number) {\n  const relative = () => (back === 0 ? msg(\"status-badge-row.today\", \"Today\") : new Intl.RelativeTimeFormat(locale, { numeric: \"always\" }).format(-back, \"day\"))\n  if (!endDate) return relative()\n  const t = Date.parse(endDate.length === 10 ? `${endDate}T00:00:00Z` : endDate)\n  if (Number.isNaN(t)) return relative()\n  // UTC, so the server and browser agree.\n  return new Intl.DateTimeFormat(locale, { month: \"short\", day: \"numeric\", timeZone: \"UTC\" }).format(new Date(t - back * 86_400_000))\n}\n\nfunction normalize(day: ServiceDay) {\n  return typeof day === \"string\" ? { status: day, note: undefined } : day\n}\n\n/** The text shown for one day, such as \"Sep 21 · Outage · Queue backlog\". */\nfunction readout(locale: string, msg: Msg, service: ServiceItem, endDate: string | undefined, index: number) {\n  const days = service.days ?? []\n  const day = normalize(days[index]!)\n  return `${dayLabel(locale, msg, endDate, days.length - 1 - index)} · ${msg.of(META[day.status].label)}${day.note ? ` · ${day.note}` : \"\"}`\n}\n\nfunction ServiceRow({ service, endDate }: { service: ServiceItem; endDate?: string }) {\n  const msg = useMessages()\n  const locale = useLocale()\n  const meta = META[service.status]\n  const Icon = meta.icon\n  const [cursor, setCursor] = React.useState<number | null>(null)\n  return (\n    <li className=\"grid gap-3 px-4 py-3.5\">\n      <div className=\"flex items-center gap-3\">\n        <div className=\"min-w-0 flex-1\">\n          <p className=\"truncate text-sm font-medium text-foreground\">{service.name}</p>\n          {service.description && <p className=\"truncate text-xs text-muted-foreground\">{service.description}</p>}\n        </div>\n        {cursor !== null && service.days ? (\n          <span aria-hidden=\"true\" className=\"truncate rounded-md bg-muted px-2 py-0.5 text-xs text-foreground\">\n            {readout(locale, msg, service, endDate, cursor)}\n          </span>\n        ) : (\n          service.uptime !== undefined && (\n            <span className=\"hidden font-mono text-xs text-muted-foreground tabular-nums sm:block\">{msg(\"status-badge-row.uptime\", \"{percent}% uptime\", { percent: service.uptime.toFixed(2) })}</span>\n          )\n        )}\n        <span\n          className={cn(\n            \"inline-flex shrink-0 items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs font-medium text-foreground\",\n            meta.chip\n          )}\n        >\n          <Icon aria-hidden=\"true\" className=\"size-3.5\" />\n          {msg.of(meta.label)}\n        </span>\n      </div>\n      <HistoryStrip service={service} endDate={endDate} cursor={cursor} setCursor={setCursor} />\n    </li>\n  )\n}\n\nfunction HistoryStrip({\n  service,\n  endDate,\n  cursor,\n  setCursor,\n}: {\n  service: ServiceItem\n  endDate?: string\n  cursor: number | null\n  setCursor: React.Dispatch<React.SetStateAction<number | null>>\n}) {\n  const dir = useDirection()\n  const msg = useMessages()\n  const locale = useLocale()\n  const days = service.days ?? []\n  const [focused, setFocused] = React.useState(false)\n  if (days.length === 0) return null\n  const last = days.length - 1\n  const at = cursor ?? last\n  const day = normalize(days[at]!)\n  const text = `${dayLabel(locale, msg, endDate, last - at)}: ${msg.of(META[day.status].label)}${day.note ? `, ${day.note}` : \"\"}`\n\n  function onKeyDown(event: React.KeyboardEvent) {\n    const step = event.shiftKey ? 7 : 1\n    const next =\n      event.key === (dir === \"rtl\" ? \"ArrowRight\" : \"ArrowLeft\") ? at - step : event.key === (dir === \"rtl\" ? \"ArrowLeft\" : \"ArrowRight\") ? at + step : event.key === \"Home\" ? 0 : event.key === \"End\" ? last : null\n    if (next === null) return\n    event.preventDefault()\n    setCursor(Math.min(last, Math.max(0, next)))\n  }\n\n  return (\n    <div className=\"relative\">\n      <div\n        role=\"slider\"\n        tabIndex={0}\n        aria-label={msg(\"status-badge-row.historyDays\", \"{name} history, {length} days\", { name: service.name, length: days.length })}\n        aria-orientation=\"horizontal\"\n        aria-valuemin={0}\n        aria-valuemax={last}\n        aria-valuenow={at}\n        aria-valuetext={text}\n        onKeyDown={onKeyDown}\n        onFocus={() => {\n          setFocused(true)\n          setCursor((c) => c ?? last)\n        }}\n        onBlur={() => {\n          setFocused(false)\n          setCursor(null)\n        }}\n        onPointerLeave={() => !focused && setCursor(null)}\n        className=\"group/strip flex h-8 items-end gap-px rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n      >\n        {days.map((raw, i) => {\n          const d = normalize(raw)\n          return (\n            <span\n              key={i}\n              onPointerEnter={() => setCursor(i)}\n              className=\"flex h-full min-w-px flex-1 items-end\"\n            >\n              <span\n                aria-hidden=\"true\"\n                className={cn(\n                  \"block w-full rounded-[2px] transition-[opacity,transform] duration-100 motion-reduce:transition-none\",\n                  META[d.status].bar,\n                  cursor !== null && cursor !== i && \"opacity-60\",\n                  cursor === i && \"scale-y-[1.08]\"\n                )}\n                style={{ height: META[d.status].height }}\n              />\n            </span>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n\ntype StatusBadgeRowProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  /** The services to list. */\n  services: ServiceItem[]\n  /** Heading above the list. */\n  title?: string\n  /** ISO date of the newest day in each `days` array, for example \"2026-09-30\". Used to label the bars. */\n  endDate?: string\n  /** Text under the heading, such as \"Updated 2 minutes ago\". */\n  updated?: string\n  /** Replaces the computed banner message. */\n  summary?: string\n}\n\nfunction StatusBadgeRow({ services, title, endDate, updated, summary, className, ...props }: StatusBadgeRowProps) {\n  const msg = useMessages()\n  const locale = useLocale()\n  title ??= msg(\"status-badge-row.title\", \"System status\")\n  const worst = services.reduce<ServiceStatus>((w, s) => (META[s.status].order > META[w].order ? s.status : w), \"operational\")\n  const bannerText =\n    summary ??\n    (worst === \"operational\"\n      ? \"All systems operational\"\n      : worst === \"maintenance\"\n        ? \"Scheduled maintenance in progress\"\n        : worst === \"degraded\"\n          ? \"Some systems are slower than usual\"\n          : \"Some systems are down\")\n  const Banner = META[worst].icon\n  const historyDays = services.reduce((n, s) => Math.max(n, s.days?.length ?? 0), 0)\n\n  return (\n    <section\n      data-slot=\"status-badge-row\"\n      data-status={worst}\n      aria-label={title}\n      className={cn(\"w-full overflow-hidden rounded-xl border bg-card text-card-foreground\", className)}\n      {...props}\n    >\n      <header className={cn(\"flex items-center gap-3 border-b px-4 py-3.5\", META[worst].chip)}>\n        <Banner aria-hidden=\"true\" className=\"size-5 shrink-0\" />\n        <div className=\"min-w-0 flex-1\" role=\"status\">\n          <h3 className=\"text-sm leading-5 font-semibold text-foreground\">{bannerText}</h3>\n          {updated && <p className=\"text-xs leading-4 text-muted-foreground\">{updated}</p>}\n        </div>\n      </header>\n      <ul className=\"divide-y\">\n        {services.map((service) => (\n          <ServiceRow key={service.name} service={service} endDate={endDate} />\n        ))}\n      </ul>\n      {historyDays > 0 && (\n        <footer className=\"flex items-center justify-between border-t bg-muted/30 px-4 py-2 text-xs text-muted-foreground\" aria-hidden=\"true\">\n          <span>{new Intl.RelativeTimeFormat(locale, { numeric: \"always\" }).format(-historyDays, \"day\")}</span>\n          <span>{msg(\"status-badge-row.today\", \"Today\")}</span>\n        </footer>\n      )}\n    </section>\n  )\n}\n\nexport { StatusBadgeRow, type StatusBadgeRowProps, type ServiceItem, type ServiceStatus, type ServiceDay }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/status-badge-row.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "status",
      "uptime",
      "incident",
      "monitoring",
      "health"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "services is [{ name, status, description?, uptime?, days? }]; days holds one status (or { status, note }) per day, oldest first. endDate labels the bars. The banner shows the worst status. Bar height as well as color encodes status.",
      "whenToUse": [
        "Public or internal status pages",
        "Admin dashboards listing service health"
      ],
      "whenNotToUse": [
        "A single presence dot (status-dot)",
        "Charting numeric metrics (chart)"
      ],
      "composesWith": [
        "status-dot",
        "badge",
        "chart"
      ],
      "a11y": [
        {
          "keys": "ArrowLeft / ArrowRight",
          "action": "Moves through days; Shift moves by a week; Home and End jump to the ends"
        },
        {
          "keys": "Screen readers",
          "action": "Each history is a slider whose value text reads 'Sep 21: Outage'"
        },
        {
          "keys": "Color",
          "action": "Status is also shown by icon, word and bar height"
        }
      ],
      "customization": [
        "endDate",
        "updated",
        "summary",
        "per-day notes"
      ]
    },
    "examples": [
      "status-badge-row-demo",
      "status-badge-row-incident"
    ],
    "url": "https://ui.ballmac.com/components/status-badge-row"
  },
  "categories": [
    "developer"
  ],
  "type": "registry:ui"
}