{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "uptime-bars",
  "type": "registry:component",
  "title": "Uptime Bars",
  "description": "A status-page block with 90 days of daily bars per service: hover or arrow along a row to read that day's uptime and incidents, with a headline that names whatever is broken right now.",
  "categories": [
    "component",
    "Status"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/uptime-bars.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { AlertTriangleIcon, CheckCircle2Icon, XCircleIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type KeyboardEvent, type PointerEvent, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface UptimeIncident {\n  daysAgo: number;\n  title: string;\n  minutes: number;\n}\n\nexport interface UptimeService {\n  id: string;\n  name: string;\n  detail?: string;\n  incidents?: UptimeIncident[];\n  missingDays?: number;\n  current?: \"operational\" | \"degraded\" | \"outage\";\n}\n\nexport interface UptimeBarsProps {\n  services?: UptimeService[];\n  days?: number;\n  endDate?: string;\n  locale?: string;\n  className?: string;\n}\n\ntype Level = \"up\" | \"minor\" | \"major\" | \"none\";\n\nconst LEVEL: Record<Level, { bar: string; label: string }> = {\n  up: { bar: \"bg-success\", label: \"Operational\" },\n  minor: { bar: \"bg-warning\", label: \"Degraded\" },\n  major: { bar: \"bg-destructive\", label: \"Outage\" },\n  none: { bar: \"bg-muted-foreground/20\", label: \"No data\" },\n};\nconst DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];\nconst COMPACT_DAYS = 45;\n\nconst defaultServices: UptimeService[] = [\n  {\n    id: \"api\",\n    name: \"API\",\n    detail: \"REST and GraphQL\",\n    incidents: [\n      { daysAgo: 71, title: \"Elevated 5xx on writes\", minutes: 22 },\n      { daysAgo: 38, title: \"Slow responses in eu-west\", minutes: 9 },\n      { daysAgo: 12, title: \"Partial outage after deploy\", minutes: 47 },\n      { daysAgo: 12, title: \"Rate limiter misconfigured\", minutes: 6 },\n    ],\n  },\n  {\n    id: \"dashboard\",\n    name: \"Dashboard\",\n    detail: \"app.fernway.dev\",\n    incidents: [{ daysAgo: 54, title: \"Login page not loading\", minutes: 18 }],\n  },\n  {\n    id: \"webhooks\",\n    name: \"Webhooks\",\n    detail: \"Event delivery\",\n    incidents: [\n      { daysAgo: 63, title: \"Delivery backlog\", minutes: 95 },\n      { daysAgo: 20, title: \"Retries delayed\", minutes: 11 },\n      { daysAgo: 0, title: \"Delayed deliveries to us-east\", minutes: 8 },\n    ],\n    current: \"degraded\",\n  },\n  {\n    id: \"auth\",\n    name: \"Auth & SSO\",\n    detail: \"SAML, OIDC, sessions\",\n    incidents: [{ daysAgo: 29, title: \"IdP metadata timeouts\", minutes: 13 }],\n  },\n  {\n    id: \"edge\",\n    name: \"Edge cache\",\n    detail: \"Assets and images\",\n    missingDays: 24,\n    incidents: [{ daysAgo: 44, title: \"Purge requests failing\", minutes: 4 }],\n  },\n];\n\nfunction RollingNumber({ text, className }: { text: string; className?: string }) {\n  const reduce = useReducedMotion();\n  let d = 0;\n  let o = 0;\n  const glyphs = [...text]\n    .reverse()\n    .map((ch) => {\n      const isDigit = ch >= \"0\" && ch <= \"9\";\n      return { ch, key: isDigit ? `d${d++}` : `s${o++}${ch}`, digit: isDigit ? Number(ch) : null };\n    })\n    .reverse();\n  return (\n    <span className={cn(\"relative inline-flex whitespace-nowrap tabular-nums leading-none\", className)}>\n      <span className=\"sr-only\">{text}</span>\n      <span aria-hidden className=\"inline-flex [--row:1.15em]\">\n        {glyphs.map((g) =>\n          g.digit === null ? (\n            <span className=\"inline-block h-(--row) leading-(--row)\" key={g.key}>\n              {g.ch}\n            </span>\n          ) : (\n            <span\n              className=\"relative inline-block h-(--row) overflow-hidden\"\n              key={g.key}\n              style={{ maskImage: \"linear-gradient(transparent, black 18%, black 82%, transparent)\" }}\n            >\n              <span className=\"invisible block leading-(--row)\">0</span>\n              <motion.span\n                animate={{ y: `${-g.digit * 10}%` }}\n                className=\"absolute inset-x-0 top-0 flex flex-col items-center\"\n                initial={false}\n                transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 260, damping: 30 }}\n              >\n                {DIGITS.map((n) => (\n                  <span className=\"block h-(--row) leading-(--row)\" key={n}>\n                    {n}\n                  </span>\n                ))}\n              </motion.span>\n            </span>\n          ),\n        )}\n      </span>\n    </span>\n  );\n}\n\nconst toUtc = (iso: string) => {\n  const [y, m, d] = iso.split(\"-\").map(Number);\n  return Date.UTC(y, m - 1, d);\n};\n\nfunction ServiceRow({\n  service,\n  days,\n  endT,\n  locale,\n}: {\n  service: UptimeService;\n  days: number;\n  endT: number;\n  locale: string;\n}) {\n  const reduce = useReducedMotion();\n  const barsRef = useRef<HTMLDivElement>(null);\n  const [active, setActive] = useState<number | null>(null);\n  const [anchor, setAnchor] = useState({ x: 0, ratio: 0.5 });\n\n  const daily = useMemo(\n    () =>\n      Array.from({ length: days }, (_, i) => {\n        const daysAgo = days - 1 - i;\n        if (i < (service.missingDays ?? 0)) return { daysAgo, level: \"none\" as Level, uptime: null, incidents: [] };\n        const incidents = (service.incidents ?? []).filter((x) => x.daysAgo === daysAgo);\n        const down = incidents.reduce((s, x) => s + x.minutes, 0);\n        const uptime = Math.max(0, 1 - down / 1440);\n        const level: Level = uptime >= 0.999 && !incidents.length ? \"up\" : uptime >= 0.98 ? \"minor\" : \"major\";\n        return { daysAgo, level, uptime, incidents };\n      }),\n    [service, days],\n  );\n  const counted = daily.filter((d) => d.uptime !== null);\n  const uptime = counted.reduce((s, d) => s + (d.uptime ?? 0), 0) / (counted.length || 1);\n  const todays = daily[days - 1];\n  const current = service.current ?? (todays.level === \"major\" ? \"outage\" : todays.level === \"minor\" ? \"degraded\" : \"operational\");\n  const date = (daysAgo: number) =>\n    new Date(endT - daysAgo * 86400000).toLocaleDateString(locale, { weekday: \"short\", month: \"short\", day: \"numeric\", timeZone: \"UTC\" });\n\n  const bars = () => Array.from(barsRef.current?.children ?? []) as HTMLElement[];\n  const firstShown = () => Math.max(0, bars().findIndex((b) => b.offsetWidth > 0));\n\n  function select(i: number) {\n    const el = bars()[i];\n    const wrap = barsRef.current;\n    if (!el || !wrap) return;\n    const x = el.offsetLeft + el.offsetWidth / 2;\n    setActive(i);\n    setAnchor({ x, ratio: x / (wrap.clientWidth || 1) });\n  }\n\n  function onPointerMove(e: PointerEvent<HTMLDivElement>) {\n    const list = bars();\n    const start = firstShown();\n    let best = start;\n    for (let i = start; i < list.length; i++) {\n      const r = list[i].getBoundingClientRect();\n      if (e.clientX >= r.left - 1) best = i;\n      else break;\n    }\n    if (best !== active) select(best);\n  }\n\n  function onKeyDown(e: KeyboardEvent<HTMLDivElement>) {\n    const cur = active ?? days - 1;\n    const lo = firstShown();\n    const next = e.key === \"ArrowLeft\" ? cur - 1 : e.key === \"ArrowRight\" ? cur + 1 : e.key === \"Home\" ? lo : e.key === \"End\" ? days - 1 : null;\n    if (e.key === \"Escape\") return setActive(null);\n    if (next === null) return;\n    e.preventDefault();\n    select(Math.max(lo, Math.min(days - 1, next)));\n  }\n\n  const day = active === null ? null : daily[active];\n  const align = anchor.ratio < 0.22 ? \"start\" : anchor.ratio > 0.78 ? \"end\" : \"center\";\n\n  return (\n    <li className=\"flex flex-col gap-2 py-3 first:pt-0 last:pb-0\">\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <div className=\"flex min-w-0 items-baseline gap-2\">\n          <span className=\"truncate font-medium text-sm\">{service.name}</span>\n          {service.detail ? <span className=\"hidden truncate text-muted-foreground text-xs @min-[26rem]:inline\">{service.detail}</span> : null}\n        </div>\n        <div className=\"flex shrink-0 items-baseline gap-2 text-xs\">\n          <span\n            className={cn(\n              \"font-medium\",\n              current === \"operational\" && \"text-success-foreground\",\n              current === \"degraded\" && \"text-warning-foreground\",\n              current === \"outage\" && \"text-destructive-foreground\",\n            )}\n          >\n            {current === \"operational\" ? \"Operational\" : current === \"degraded\" ? \"Degraded\" : \"Outage\"}\n          </span>\n          <span className=\"text-muted-foreground tabular-nums\">{uptime >= 0.99995 ? \"100\" : (uptime * 100).toFixed(2)}%</span>\n        </div>\n      </div>\n\n      <div className=\"relative\">\n        <div\n          aria-label={`${service.name}, ${days} days, ${(uptime * 100).toFixed(2)}% uptime. Arrow keys to inspect days.`}\n          className=\"flex h-8 cursor-default touch-pan-y gap-[2px] rounded-sm outline-none ring-offset-2 ring-offset-card focus-visible:ring-2 focus-visible:ring-ring\"\n          onBlur={() => setActive(null)}\n          onFocus={(e) => e.currentTarget.matches(\":focus-visible\") && select(days - 1)}\n          onKeyDown={onKeyDown}\n          onPointerCancel={() => setActive(null)}\n          onPointerDown={onPointerMove}\n          onPointerLeave={(e) => e.pointerType === \"mouse\" && setActive(null)}\n          onPointerMove={onPointerMove}\n          ref={barsRef}\n          role=\"application\"\n          tabIndex={0}\n        >\n          {daily.map((d, i) => (\n            <motion.span\n              animate={{ scaleY: 1 }}\n              className={cn(\n                \"relative h-full min-w-0 flex-1 rounded-[calc(var(--radius)*0.2)]\",\n                LEVEL[d.level].bar,\n                active !== null && active !== i && \"opacity-45\",\n                i < days - COMPACT_DAYS && \"@max-[26rem]:hidden\",\n              )}\n              initial={{ scaleY: 0.3 }}\n              key={d.daysAgo}\n              style={{ originY: 1 }}\n              transition={{ duration: reduce ? 0 : 0.3, delay: reduce ? 0 : Math.min(0.4, i * 0.004), ease: [0.23, 1, 0.32, 1] }}\n            />\n          ))}\n        </div>\n\n        <AnimatePresence>\n          {day ? (\n            <motion.div\n              animate={{ opacity: 1, scale: 1 }}\n              className={cn(\n                \"pointer-events-none absolute bottom-full z-20 mb-2 w-56 rounded-xl border bg-popover p-2.5 text-popover-foreground text-xs shadow-lg/5\",\n                align === \"center\" && \"-translate-x-1/2\",\n                align === \"end\" && \"-translate-x-full\",\n              )}\n              exit={{ opacity: 0, transition: { duration: reduce ? 0 : 0.1 } }}\n              initial={{ opacity: 0, scale: reduce ? 1 : 0.96 }}\n              style={{\n                left: anchor.x + (align === \"start\" ? -6 : align === \"end\" ? 6 : 0),\n                originX: align === \"start\" ? 0 : align === \"end\" ? 1 : 0.5,\n                originY: 1,\n              }}\n              transition={{ duration: reduce ? 0 : 0.14, ease: [0.23, 1, 0.32, 1] }}\n            >\n              <div className=\"flex items-baseline justify-between gap-2\">\n                <span className=\"font-medium\">{day.daysAgo === 0 ? \"Today\" : date(day.daysAgo)}</span>\n                <span className=\"text-muted-foreground tabular-nums\">\n                  {day.uptime === null ? \"No data\" : `${(day.uptime * 100).toFixed(2)}%`}\n                </span>\n              </div>\n              {day.incidents.length ? (\n                <ul className=\"mt-2 flex flex-col gap-1.5 border-t pt-2\">\n                  {day.incidents.map((x) => (\n                    <li className=\"flex items-start gap-2\" key={x.title}>\n                      <span\n                        className={cn(\n                          \"mt-1 size-1.5 shrink-0 rounded-full\",\n                          x.minutes >= 30 ? \"bg-destructive\" : \"bg-warning\",\n                        )}\n                      />\n                      <span className=\"min-w-0 flex-1 text-pretty\">{x.title}</span>\n                      <span className=\"shrink-0 text-muted-foreground tabular-nums\">\n                        {x.minutes >= 60 ? `${Math.floor(x.minutes / 60)}h ${x.minutes % 60}m` : `${x.minutes}m`}\n                      </span>\n                    </li>\n                  ))}\n                </ul>\n              ) : (\n                <p className=\"mt-1 text-muted-foreground\">\n                  {day.uptime === null ? \"Monitoring started later\" : \"No incidents reported\"}\n                </p>\n              )}\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n      </div>\n    </li>\n  );\n}\n\nexport function UptimeBars({ services = defaultServices, days = 90, endDate = \"2026-10-24\", locale = \"en-US\", className }: UptimeBarsProps) {\n  const endT = toUtc(endDate);\n  const states = services.map((s) => {\n    if (s.current) return s.current;\n    const today = (s.incidents ?? []).filter((x) => x.daysAgo === 0).reduce((m, x) => m + x.minutes, 0);\n    return today === 0 ? \"operational\" : today >= 30 ? \"outage\" : \"degraded\";\n  });\n  const worst = states.includes(\"outage\") ? \"outage\" : states.includes(\"degraded\") ? \"degraded\" : \"operational\";\n  const overall = useMemo(() => {\n    let sum = 0;\n    let count = 0;\n    for (const s of services) {\n      for (let i = s.missingDays ?? 0; i < days; i++) {\n        const daysAgo = days - 1 - i;\n        const down = (s.incidents ?? []).filter((x) => x.daysAgo === daysAgo).reduce((m, x) => m + x.minutes, 0);\n        sum += Math.max(0, 1 - down / 1440);\n        count += 1;\n      }\n    }\n    return count ? sum / count : 1;\n  }, [services, days]);\n  const affected = services.filter((_, i) => states[i] !== \"operational\");\n  const totalIncidents = services.reduce((s, x) => s + (x.incidents?.length ?? 0), 0);\n\n  return (\n    <section\n      aria-label=\"Service status\"\n      className={cn(\"@container w-full max-w-[540px] rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}\n    >\n      <header className=\"flex items-start justify-between gap-3 border-b p-5\">\n        <div className=\"flex min-w-0 items-start gap-2.5\">\n          <span\n            className={cn(\n              \"mt-0.5 flex size-5 shrink-0 items-center justify-center [&_svg]:size-5\",\n              worst === \"operational\" && \"text-success\",\n              worst === \"degraded\" && \"text-warning\",\n              worst === \"outage\" && \"text-destructive\",\n            )}\n          >\n            {worst === \"operational\" ? <CheckCircle2Icon /> : worst === \"degraded\" ? <AlertTriangleIcon /> : <XCircleIcon />}\n          </span>\n          <div className=\"flex min-w-0 flex-col gap-0.5\">\n            <h3 className=\"text-balance font-medium text-sm\">\n              {worst === \"operational\"\n                ? \"All systems operational\"\n                : `${worst === \"outage\" ? \"Outage\" : \"Degraded performance\"} on ${affected.map((s) => s.name).join(\", \")}`}\n            </h3>\n            <p className=\"text-muted-foreground text-xs tabular-nums\">\n              {totalIncidents} incidents · last {days} days\n            </p>\n          </div>\n        </div>\n        <div className=\"flex shrink-0 flex-col items-end gap-0.5\">\n          <RollingNumber className=\"font-semibold text-lg tracking-tight\" text={`${(overall * 100).toFixed(2)}%`} />\n          <span className=\"text-muted-foreground text-xs\">{days}-day uptime</span>\n        </div>\n      </header>\n\n      <ul className=\"flex flex-col divide-y px-5 pt-4 pb-3\">\n        {services.map((s) => (\n          <ServiceRow days={days} endT={endT} key={s.id} locale={locale} service={s} />\n        ))}\n      </ul>\n\n      <footer className=\"flex items-center justify-between gap-3 px-5 pb-4 text-muted-foreground text-xs\">\n        <span className=\"@max-[26rem]:hidden\">{days} days ago</span>\n        <span className=\"@min-[26rem]:hidden\">{Math.min(days, COMPACT_DAYS)} days ago</span>\n        <div className=\"hidden items-center gap-3 @min-[26rem]:flex\">\n          {([\"up\", \"minor\", \"major\"] as Level[]).map((l) => (\n            <span className=\"inline-flex items-center gap-1.5\" key={l}>\n              <span className={cn(\"h-2.5 w-1 rounded-full\", LEVEL[l].bar)} />\n              {LEVEL[l].label}\n            </span>\n          ))}\n        </div>\n        <span>Today</span>\n      </footer>\n    </section>\n  );\n}\n"
    }
  ]
}