{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "uptime-01",
  "type": "registry:block",
  "title": "Service Uptime & Incidents",
  "description": "A service availability card with daily status history and inspectable incident details, adapted from Kokonut UI.",
  "author": "kokonutUI",
  "registryDependencies": [],
  "dependencies": [
    "cn@0.2.6",
    "lucide-react@0.577.0"
  ],
  "files": [
    {
      "path": "licenses/uptime-01/LICENSE.txt",
      "type": "registry:file",
      "target": "@components/uiblocks/uptime-01/LICENSE.txt",
      "content": "Includes source from Blocks.so and its bundled primitives.\nSource: https://github.com/ephraimduncan/blocks\nRevision: 3f759d5171c2391253ec4c772e9e5b3db2bc7c7f\nUIBlocks modifications (block v0.1.0):\n- Added explicit host-provided daily statuses and uptime values; unreported data stays unknown.\n- Kept deterministic sample history and a fixed reporting date behind explicit labeled demo mode.\n- Retained keyboard and pointer inspection of daily status and incident details.\n\nMIT License\n\nCopyright (c) 2025 Ephraim Duncan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\nThird-party design: Kokonut UI\nBlock: uptime-01 (v0.1.0)\nSource: https://github.com/kokonut-labs/kokonutui\nAuthor: kokonutUI\nRevision: 340d5741a1a48114b2765c2abf03d50225d52602\nOriginal files:\n- components/kokonutui/uptime-card.tsx\nUIBlocks modifications:\n- Added explicit host-provided daily statuses and uptime values; unreported data stays unknown.\n- Kept deterministic sample history and a fixed reporting date behind explicit labeled demo mode.\n- Retained keyboard and pointer inspection of daily status and incident details.\n\nMIT License\n\nCopyright (c) 2025 kokonutUI\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
    },
    {
      "path": "lib/utils.ts",
      "type": "registry:component",
      "target": "@components/uiblocks/uptime-01/shared/lib/utils.ts",
      "content": "export { cn } from \"cn\";\n"
    },
    {
      "path": "content/components/stats/uptime-01.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/uptime-01/uptime-01.tsx",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Uptime Card\n * @version: 1.0.0\n * @date: 2026-10-15\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n * @source: components/kokonutui/uptime-card.tsx\n * @revision: 340d5741a1a48114b2765c2abf03d50225d52602\n * Adapted for UIBlocks: host-reported states, explicit demo data, and UTC dates.\n */\n\nimport { CircleCheck, TriangleAlert, X } from \"lucide-react\";\nimport {\n  type KeyboardEvent,\n  type PointerEvent,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"./shared/lib/utils\";\n\n// How much wider the bar under the pointer gets, and how far the swell\n// spreads, in bars.\nconst MAGNIFY = 5;\nconst SPREAD = 2.4;\n// Bars ease toward their width at this rate, per second.\nconst EASE = 18;\nconst DAY = 86_400_000;\n\nexport type DayStatus = \"up\" | \"degraded\" | \"down\" | \"none\";\n\nexport interface IncidentUpdate {\n  /** HH:MM, local to the status page. */\n  time: string;\n  label: string;\n  note?: string;\n}\n\nexport interface Incident {\n  title: string;\n  status: Exclude<DayStatus, \"up\" | \"none\">;\n  /** Minutes of impact that day. */\n  minutes: number;\n  updates: IncidentUpdate[];\n}\n\nexport interface UptimeService {\n  name: string;\n  /** Host-reported daily states, oldest first. Missing values mean no data. */\n  statuses?: readonly DayStatus[];\n  /** The host-computed availability percentage for this complete reporting window. */\n  uptime?: number;\n  /** Incidents by day index, 0 being the oldest day shown. */\n  incidents?: Record<number, Incident>;\n  /** Days with no data (before the service existed), from the start. */\n  missing?: number;\n}\n\nexport interface UptimeCardProps {\n  mode?: \"controlled\" | \"demo\";\n  title?: string;\n  services?: readonly UptimeService[];\n  days?: number;\n  /** Required for host reports. The last day shown, as a UTC YYYY-MM-DD. */\n  endDate?: string;\n  className?: string;\n}\n\nconst DEFAULT_SERVICES: UptimeService[] = [\n  {\n    name: \"API\",\n    incidents: {\n      19: {\n        title: \"Elevated error rates on writes\",\n        status: \"degraded\",\n        minutes: 18,\n        updates: [\n          {\n            time: \"14:02\",\n            label: \"Investigating\",\n            note: \"5xx on POST rose to 2%.\",\n          },\n          {\n            time: \"14:11\",\n            label: \"Identified\",\n            note: \"A bad config push to one region.\",\n          },\n          {\n            time: \"14:20\",\n            label: \"Resolved\",\n            note: \"Rolled back; error rates normal.\",\n          },\n        ],\n      },\n      84: {\n        title: \"Slow responses in eu-west\",\n        status: \"degraded\",\n        minutes: 23,\n        updates: [\n          {\n            time: \"09:41\",\n            label: \"Investigating\",\n            note: \"p95 latency up 3x in eu-west.\",\n          },\n          {\n            time: \"09:52\",\n            label: \"Monitoring\",\n            note: \"Added capacity to the pool.\",\n          },\n          { time: \"10:04\", label: \"Resolved\" },\n        ],\n      },\n    },\n  },\n  {\n    name: \"Dashboard\",\n    incidents: {\n      43: {\n        title: \"Charts failed to load\",\n        status: \"degraded\",\n        minutes: 12,\n        updates: [\n          { time: \"18:30\", label: \"Investigating\" },\n          {\n            time: \"18:42\",\n            label: \"Resolved\",\n            note: \"A CDN cache rule was reverted.\",\n          },\n        ],\n      },\n    },\n  },\n  {\n    name: \"Webhooks\",\n    incidents: {\n      69: {\n        title: \"Deliveries delayed\",\n        status: \"down\",\n        minutes: 48,\n        updates: [\n          {\n            time: \"02:15\",\n            label: \"Investigating\",\n            note: \"The delivery queue stopped draining.\",\n          },\n          {\n            time: \"02:38\",\n            label: \"Identified\",\n            note: \"A worker deploy failed health checks.\",\n          },\n          {\n            time: \"03:03\",\n            label: \"Resolved\",\n            note: \"Backlog delivered in order.\",\n          },\n        ],\n      },\n    },\n  },\n  {\n    name: \"Database\",\n    missing: 6,\n    incidents: {\n      27: {\n        title: \"Failover to replica\",\n        status: \"down\",\n        minutes: 9,\n        updates: [\n          {\n            time: \"22:07\",\n            label: \"Investigating\",\n            note: \"Primary stopped accepting writes.\",\n          },\n          {\n            time: \"22:16\",\n            label: \"Resolved\",\n            note: \"Promoted the replica; no data lost.\",\n          },\n        ],\n      },\n    },\n  },\n];\n\nconst clamp = (v: number, lo: number, hi: number) =>\n  Math.min(hi, Math.max(lo, v));\n\nconst formatDay = (ms: number) =>\n  new Date(ms).toLocaleDateString(\"en-US\", {\n    weekday: \"short\",\n    month: \"short\",\n    day: \"numeric\",\n    timeZone: \"UTC\",\n  });\n\nfunction statusOf(service: UptimeService, day: number): DayStatus {\n  if (day < (service.missing ?? 0)) {\n    return \"none\";\n  }\n  const reported = service.statuses?.[day];\n  return (\n    service.incidents?.[day]?.status ??\n    (reported === \"up\" || reported === \"degraded\" || reported === \"down\"\n      ? reported\n      : \"none\")\n  );\n}\n\n/** Illustrative availability for the explicit demo only, never a host metric. */\nfunction demoUptimeOf(service: UptimeService, days: number) {\n  const tracked = days - (service.missing ?? 0);\n  let lost = 0;\n  for (const [index, incident] of Object.entries(service.incidents ?? {})) {\n    if (Number(index) >= (service.missing ?? 0) && Number(index) < days)\n      lost += incident.minutes * (incident.status === \"down\" ? 1 : 0.5);\n  }\n  return tracked > 0 ? clamp(100 * (1 - lost / (tracked * 1440)), 0, 100) : 0;\n}\n\n/** Target widths: a swell around the focus, flat without one. */\nfunction swell(count: number, focus: number | null, out: Float64Array) {\n  for (let i = 0; i < count; i++) {\n    out[i] =\n      focus === null\n        ? 1\n        : 1 + MAGNIFY * Math.exp(-(((i - focus) / SPREAD) ** 2));\n  }\n  return out;\n}\n\n/** The bar under x (0 to 1) when bars have these widths. */\nfunction barAt(weights: Float64Array, x: number) {\n  let total = 0;\n  for (const w of weights) {\n    total += w;\n  }\n  let at = x * total;\n  for (let i = 0; i < weights.length; i++) {\n    at -= weights[i];\n    if (at <= 0) {\n      return i;\n    }\n  }\n  return weights.length - 1;\n}\n\nfunction incidentsOn(services: readonly UptimeService[], day: number) {\n  return services.flatMap((s) => {\n    const incident = s.incidents?.[day];\n    return incident ? [{ service: s.name, incident }] : [];\n  });\n}\n\nfunction describeDay(list: { service: string; incident: Incident }[]) {\n  const first = list[0];\n  if (!first) {\n    return \"No incidents reported\";\n  }\n  const how = first.incident.status === \"down\" ? \"down\" : \"degraded\";\n  const more = list.length > 1 ? `, +${list.length - 1} more` : \"\";\n  return `${first.service} ${how} for ${first.incident.minutes} min${more}`;\n}\n\nconst BAR_COLOR: Record<DayStatus, string> = {\n  up: \"bg-emerald-500 dark:bg-emerald-400\",\n  degraded: \"bg-amber-400 dark:bg-amber-300\",\n  down: \"bg-rose-500 dark:bg-rose-400\",\n  none: \"bg-zinc-200 dark:bg-zinc-800\",\n};\n\nconst STATUS_TEXT: Record<DayStatus, string> = {\n  up: \"Operational\",\n  degraded: \"Degraded\",\n  down: \"Outage\",\n  none: \"No data\",\n};\n\nconst EMPTY_SERVICES: readonly UptimeService[] = [];\n\n/** Strict ISO date validation keeps server/client dates stable across time zones. */\nexport function uptimeEndDate(value: string | undefined): number | null {\n  if (!value || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return null;\n  const date = Date.parse(`${value}T00:00:00Z`);\n  return Number.isFinite(date) &&\n    new Date(date).toISOString().slice(0, 10) === value\n    ? date\n    : null;\n}\n\nexport default function UptimeCard({\n  mode = \"controlled\",\n  title = \"System status\",\n  services: suppliedServices,\n  days: suppliedDays = 90,\n  endDate: suppliedEndDate,\n  className,\n}: UptimeCardProps) {\n  const days = Number.isFinite(suppliedDays)\n    ? clamp(Math.floor(suppliedDays), 1, 366)\n    : 90;\n  const endDate =\n    suppliedEndDate ?? (mode === \"demo\" ? \"2026-10-14\" : undefined);\n  const services = useMemo(\n    () =>\n      suppliedServices ??\n      (mode === \"demo\"\n        ? DEFAULT_SERVICES.map((service) => ({\n            ...service,\n            statuses: Array.from(\n              { length: days },\n              (_, day): DayStatus =>\n                day < (service.missing ?? 0)\n                  ? \"none\"\n                  : (service.incidents?.[day]?.status ?? \"up\")\n            ),\n            uptime: demoUptimeOf(service, days),\n          }))\n        : EMPTY_SERVICES),\n    [suppliedServices, mode, days]\n  );\n  const parsedEnd = uptimeEndDate(endDate);\n  const hasReport = services.length > 0 && parsedEnd !== null;\n  const [active, setActive] = useState<number | null>(null);\n  const [pinned, setPinned] = useState<number | null>(null);\n  const barsRef = useRef<HTMLDivElement>(null);\n  const sim = useRef({\n    weights: new Float64Array(days).fill(1),\n    target: new Float64Array(days).fill(1),\n    focus: null as number | null,\n    raf: 0,\n    last: 0,\n    reduceMotion: false,\n  });\n  if (sim.current.weights.length !== days) {\n    sim.current.weights = new Float64Array(days).fill(1);\n    sim.current.target = new Float64Array(days).fill(1);\n  }\n\n  const end = parsedEnd ?? 0;\n  const dateOf = useCallback(\n    (day: number) => end - (days - 1 - day) * DAY,\n    [end, days]\n  );\n  const uptimes = useMemo(\n    () =>\n      services.map((service) =>\n        typeof service.uptime === \"number\" &&\n        Number.isFinite(service.uptime) &&\n        service.uptime >= 0 &&\n        service.uptime <= 100\n          ? service.uptime\n          : null\n      ),\n    [services, days]\n  );\n  const openIssues = services.filter(\n    (s) => statusOf(s, days - 1) !== \"up\" && statusOf(s, days - 1) !== \"none\"\n  );\n\n  // Widths are written straight to the bars, one flex-grow per bar.\n  const paint = useCallback(() => {\n    const rows = barsRef.current?.querySelectorAll<HTMLElement>(\"[data-row]\");\n    const { weights } = sim.current;\n    for (const row of rows ?? []) {\n      const bars = row.children;\n      for (let i = 0; i < bars.length; i++) {\n        (bars[i] as HTMLElement).style.flexGrow = (weights[i] ?? 1).toFixed(3);\n      }\n    }\n  }, []);\n\n  const tick = useCallback(\n    (now: number) => {\n      const s = sim.current;\n      s.raf = 0;\n      const dt = s.last ? Math.min((now - s.last) / 1000, 1 / 30) : 1 / 60;\n      s.last = now;\n      const k = 1 - Math.exp(-EASE * dt);\n      let moving = false;\n      for (let i = 0; i < s.weights.length; i++) {\n        const gap = s.target[i] - s.weights[i];\n        if (Math.abs(gap) > 0.002) {\n          s.weights[i] += gap * k;\n          moving = true;\n        } else {\n          s.weights[i] = s.target[i];\n        }\n      }\n      paint();\n      if (moving) {\n        s.raf = requestAnimationFrame(tick);\n      } else {\n        s.last = 0;\n      }\n    },\n    [paint]\n  );\n\n  const focusOn = useCallback(\n    (focus: number | null) => {\n      const s = sim.current;\n      s.focus = focus;\n      swell(s.weights.length, focus, s.target);\n      if (s.reduceMotion) {\n        s.weights.set(s.target);\n        paint();\n      } else if (!s.raf) {\n        s.raf = requestAnimationFrame(tick);\n      }\n    },\n    [paint, tick]\n  );\n\n  useEffect(() => {\n    const s = sim.current;\n    s.reduceMotion = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n    return () => cancelAnimationFrame(s.raf);\n  }, []);\n\n  /** The day under the pointer, found against the swollen layout. */\n  const dayAt = (clientX: number) => {\n    const rect = barsRef.current?.getBoundingClientRect();\n    if (!rect) {\n      return 0;\n    }\n    const x = clamp((clientX - rect.left) / rect.width, 0, 1);\n    const probe = new Float64Array(days);\n    let day = Math.round(x * (days - 1));\n    // Centre the swell on the bar that ends up under the pointer.\n    for (let pass = 0; pass < 3; pass++) {\n      day = barAt(swell(days, day, probe), x);\n    }\n    return day;\n  };\n\n  const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {\n    if (\n      event.pointerType !== \"mouse\" &&\n      !event.currentTarget.hasPointerCapture(event.pointerId)\n    ) {\n      return;\n    }\n    const day = dayAt(event.clientX);\n    // Most moves stay on the same bar: skip the re-render and the retarget.\n    if (day === active) {\n      return;\n    }\n    setActive(day);\n    focusOn(day);\n  };\n\n  const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {\n    if (event.pointerType === \"mouse\" && event.button !== 0) {\n      return;\n    }\n    if (event.pointerType !== \"mouse\") {\n      event.currentTarget.setPointerCapture(event.pointerId);\n    }\n    const day = dayAt(event.clientX);\n    setActive(day);\n    focusOn(day);\n  };\n\n  const handleClick = () => {\n    if (active !== null) {\n      setPinned((p) => (p === active ? null : active));\n    }\n  };\n\n  const leave = () => {\n    setActive(null);\n    focusOn(pinned);\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    const from = active ?? pinned ?? days - 1;\n    const moves: Record<string, number> = {\n      ArrowLeft: -1,\n      ArrowRight: 1,\n      Home: -days,\n      End: days,\n    };\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      setPinned((p) => (p === from ? null : from));\n      return;\n    }\n    if (event.key === \"Escape\") {\n      setPinned(null);\n      setActive(null);\n      focusOn(null);\n      return;\n    }\n    const move = moves[event.key];\n    if (move === undefined) {\n      return;\n    }\n    event.preventDefault();\n    const day = clamp(from + move, 0, days - 1);\n    setActive(day);\n    focusOn(day);\n  };\n\n  const selectedDay = active ?? pinned;\n  const shownDay =\n    selectedDay === null ? null : clamp(selectedDay, 0, days - 1);\n  const shownIncidents =\n    shownDay === null ? [] : incidentsOn(services, shownDay);\n  const summary =\n    shownDay === null\n      ? \"Hover or use arrow keys to inspect a day\"\n      : `${formatDay(dateOf(shownDay))} · ${services.map((service) => `${service.name}: ${STATUS_TEXT[statusOf(service, shownDay)]}`).join(\"; \")}${shownIncidents.length ? `. ${describeDay(shownIncidents)}` : \"\"}`;\n  // The log keeps showing the last pinned day while it collapses.\n  const lastPinned = useRef<number | null>(null);\n  if (pinned !== null) {\n    lastPinned.current = pinned;\n  }\n  const selectedLogDay = pinned ?? lastPinned.current;\n  const logDay =\n    selectedLogDay === null ? null : clamp(selectedLogDay, 0, days - 1);\n  const pinnedIncidents = logDay === null ? [] : incidentsOn(services, logDay);\n\n  if (!hasReport) {\n    return (\n      <section\n        data-uptime-mode={mode}\n        className={cn(\n          \"mx-auto w-full max-w-xl rounded-3xl border border-zinc-200 bg-white p-5 text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100\",\n          className\n        )}\n      >\n        <h2 className=\"break-words text-sm font-medium\">{title}</h2>\n        {mode === \"demo\" && (\n          <p className=\"mt-2 text-xs text-zinc-500\">\n            Demo data · Illustrative monitoring history.\n          </p>\n        )}\n        <p className=\"mt-3 text-sm text-zinc-500 dark:text-zinc-400\">\n          {services.length\n            ? \"Supply a valid reporting end date (YYYY-MM-DD).\"\n            : \"No monitoring data supplied. Connect your status feed.\"}\n        </p>\n      </section>\n    );\n  }\n\n  return (\n    <div\n      data-uptime-mode={mode}\n      className={cn(\n        \"mx-auto w-full max-w-xl select-none rounded-3xl border border-zinc-200 bg-white p-5 text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100\",\n        className\n      )}\n    >\n      {mode === \"demo\" && (\n        <p className=\"mb-3 text-xs text-zinc-500 dark:text-zinc-400\">\n          Demo data · Illustrative monitoring history. No live service\n          connection.\n        </p>\n      )}\n      <div className=\"flex flex-wrap items-center justify-between gap-3\">\n        <h2 className=\"min-w-0 break-words font-medium text-sm\">{title}</h2>\n        {openIssues.length ? (\n          <p className=\"flex items-center gap-1.5 text-amber-600 text-sm dark:text-amber-400\">\n            <TriangleAlert aria-hidden className=\"size-4\" />\n            {openIssues.length} service{openIssues.length > 1 ? \"s\" : \"\"}{\" \"}\n            affected\n          </p>\n        ) : services.some(\n            (service) => statusOf(service, days - 1) === \"none\"\n          ) ? (\n          <p className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n            Current status not reported\n          </p>\n        ) : (\n          <p className=\"flex items-center gap-1.5 text-emerald-600 text-sm dark:text-emerald-400\">\n            <CircleCheck aria-hidden className=\"size-4\" />\n            All systems operational\n          </p>\n        )}\n      </div>\n      <p\n        aria-live=\"polite\"\n        title={summary}\n        className=\"mt-1 truncate text-sm text-zinc-500 tabular-nums dark:text-zinc-400\"\n      >\n        {summary}\n      </p>\n\n      <div\n        aria-label={`Daily status for the last ${days} days. Use the arrow keys to move between days and Enter to open one.`}\n        aria-valuemax={days}\n        aria-valuemin={1}\n        aria-valuenow={(shownDay ?? days - 1) + 1}\n        aria-valuetext={summary}\n        className=\"mt-4 cursor-pointer touch-pan-y rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-4 dark:focus-visible:ring-offset-zinc-950\"\n        onBlur={() => {\n          if (active !== null) {\n            leave();\n          }\n        }}\n        onClick={handleClick}\n        onKeyDown={handleKeyDown}\n        onLostPointerCapture={(event) => {\n          // A touch's implicit capture on the bar it started on moves to this\n          // element on pointerdown; that bubbling event is not the end.\n          if (event.target === event.currentTarget) {\n            leave();\n          }\n        }}\n        onPointerDown={handlePointerDown}\n        onPointerLeave={(event) => {\n          if (event.pointerType === \"mouse\") {\n            leave();\n          }\n        }}\n        onPointerMove={handlePointerMove}\n        ref={barsRef}\n        role=\"slider\"\n        tabIndex={0}\n      >\n        {services.map((service, row) => (\n          <div className={cn(row > 0 && \"mt-3\")} key={service.name}>\n            <div className=\"mb-1.5 flex justify-between gap-3 text-xs\">\n              <span\n                className=\"min-w-0 truncate font-medium\"\n                title={service.name}\n              >\n                {service.name}\n              </span>\n              <span className=\"text-zinc-500 tabular-nums dark:text-zinc-400\">\n                {uptimes[row] === null\n                  ? \"Not reported\"\n                  : `${uptimes[row].toFixed(2)}%`}\n              </span>\n            </div>\n            <div className=\"flex h-7 gap-px sm:gap-[2px]\" data-row>\n              {Array.from({ length: days }, (_, day) => {\n                const status = statusOf(service, day);\n                const lit = day === shownDay;\n                return (\n                  <div\n                    className={cn(\n                      \"min-w-0 basis-0 rounded-[2px] transition-opacity duration-150 ease-out\",\n                      BAR_COLOR[status],\n                      shownDay !== null && !lit && \"opacity-60\"\n                    )}\n                    // biome-ignore lint/suspicious/noArrayIndexKey: one bar per day, never reordered\n                    key={day}\n                    style={{ flexGrow: 1 }}\n                  />\n                );\n              })}\n            </div>\n          </div>\n        ))}\n        <div className=\"mt-2 flex justify-between text-xs text-zinc-400 dark:text-zinc-500\">\n          <span>{formatDay(dateOf(0))}</span>\n          <span>{formatDay(end)}</span>\n        </div>\n      </div>\n\n      {/* A pinned day opens its incident log. */}\n      <div\n        className={cn(\n          \"grid transition-[grid-template-rows,opacity] duration-[260ms] ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\",\n          pinned === null\n            ? \"grid-rows-[0fr] opacity-0\"\n            : \"grid-rows-[1fr] opacity-100\"\n        )}\n        inert={pinned === null}\n      >\n        <div className=\"overflow-hidden\">\n          {logDay !== null && (\n            <div className=\"mt-4 rounded-2xl bg-zinc-50 p-4 dark:bg-zinc-900\">\n              <div className=\"flex items-center justify-between\">\n                <p className=\"font-medium text-sm\">\n                  {formatDay(dateOf(logDay))}\n                </p>\n                <button\n                  aria-label=\"Close day\"\n                  className=\"rounded-full p-1 text-zinc-500 outline-none transition-colors duration-150 ease-out hover:bg-zinc-200 hover:text-zinc-900 focus-visible:ring-2 focus-visible:ring-zinc-400 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100\"\n                  onClick={() => {\n                    barsRef.current?.focus({ preventScroll: true });\n                    setPinned(null);\n                    focusOn(null);\n                  }}\n                  type=\"button\"\n                >\n                  <X aria-hidden className=\"size-3.5\" />\n                </button>\n              </div>\n              {pinnedIncidents.length === 0 && (\n                <p className=\"mt-1 text-sm text-zinc-500 dark:text-zinc-400\">\n                  No incidents were supplied for this day.\n                </p>\n              )}\n              {pinnedIncidents.map(({ service, incident }) => (\n                <div className=\"mt-3\" key={service}>\n                  <p className=\"flex flex-wrap items-center gap-2 break-words text-sm [overflow-wrap:anywhere]\">\n                    <span\n                      className={cn(\n                        \"rounded-full px-2 py-0.5 font-medium text-[11px]\",\n                        incident.status === \"down\"\n                          ? \"bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300\"\n                          : \"bg-amber-100 text-amber-800 dark:bg-amber-400/15 dark:text-amber-200\"\n                      )}\n                    >\n                      {STATUS_TEXT[incident.status]}\n                    </span>\n                    <span className=\"font-medium\">{service}</span>\n                    <span className=\"text-zinc-500 dark:text-zinc-400\">\n                      {incident.title} · {incident.minutes} min\n                    </span>\n                  </p>\n                  <ol className=\"mt-2.5 ml-1 border-zinc-200 border-l dark:border-zinc-800\">\n                    {incident.updates.map((update) => (\n                      <li\n                        className=\"relative pb-2.5 pl-4 text-sm [overflow-wrap:anywhere] last:pb-0\"\n                        key={`${update.time}-${update.label}`}\n                      >\n                        <span className=\"absolute top-[7px] -left-[3.5px] size-1.5 rounded-full bg-zinc-400 dark:bg-zinc-500\" />\n                        <span className=\"text-zinc-400 tabular-nums dark:text-zinc-500\">\n                          {update.time}\n                        </span>{\" \"}\n                        <span className=\"font-medium\">{update.label}</span>\n                        {update.note && (\n                          <span className=\"text-zinc-500 dark:text-zinc-400\">\n                            {\" \"}\n                            {update.note}\n                          </span>\n                        )}\n                      </li>\n                    ))}\n                  </ol>\n                </div>\n              ))}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
    }
  ],
  "categories": [
    "stats",
    "application",
    "clean-saas"
  ],
  "docs": "Import the default component from @components/uiblocks/uptime-01/uptime-01.tsx. React 19, Tailwind CSS 4, and the standard shadcn CSS theme variables are required. This block is framework-independent React and works in TanStack Start, Vite, and Next.js projects. Connect links or navigation callbacks to your host application. The live preview opts into mode='demo'; the installed component defaults to controlled data and actions. Supply the host's records and callbacks before enabling service actions. Original design: Kokonut UI by kokonutUI, commit 340d5741a1a48114b2765c2abf03d50225d52602, source https://github.com/kokonut-labs/kokonutui. Original paths and local adaptations are recorded in meta.source. The complete upstream MIT notice is bundled in LICENSE.txt; retain it when adapting the component. See https://uiblocks.org/stats/uptime-01 for content guidelines and source attribution.",
  "meta": {
    "kind": "block",
    "previewProps": {
      "mode": "demo"
    },
    "defaultMode": "controlled",
    "demoMode": "explicit",
    "includesBackend": false,
    "version": "0.1.0",
    "source": {
      "name": "Kokonut UI",
      "url": "https://github.com/kokonut-labs/kokonutui",
      "license": "MIT",
      "author": "kokonutUI",
      "revision": "340d5741a1a48114b2765c2abf03d50225d52602",
      "licenseFile": "licenses/third-party/kokonut-ui-MIT.txt",
      "files": [
        "components/kokonutui/uptime-card.tsx"
      ],
      "modifications": [
        "Added explicit host-provided daily statuses and uptime values; unreported data stays unknown.",
        "Kept deterministic sample history and a fixed reporting date behind explicit labeled demo mode.",
        "Retained keyboard and pointer inspection of daily status and incident details."
      ]
    },
    "license": "MIT",
    "style": "clean-saas",
    "pageType": "application",
    "featured": true,
    "tags": [
      "uptime",
      "status",
      "service",
      "monitoring",
      "incidents",
      "availability",
      "服务状态",
      "可用性",
      "监控",
      "事故",
      "状态页"
    ],
    "contentGuidelines": [
      "Provide actual monitoring history, incidents, percentage values and the reporting endDate from your service.",
      "Missing history must remain No data and missing uptime Not reported; do not infer availability from absent incidents.",
      "Use mode='demo' only to inspect fictional sample history. Check status labels without relying on color alone."
    ],
    "recommendedWith": [
      "notification-stack-01",
      "stats-11",
      "dashboard-01"
    ],
    "entry": "@components/uiblocks/uptime-01/uptime-01.tsx",
    "exportName": "default",
    "frameworkIndependent": true,
    "frameworkDependencies": [],
    "compatibleHosts": [
      "react",
      "vite",
      "tanstack-start",
      "nextjs"
    ],
    "requirements": {
      "react": ">=19",
      "tailwind": ">=4"
    },
    "previewUrl": "https://uiblocks.org/preview/uptime-01"
  }
}
