{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "uptime-bar",
  "title": "Uptime Bar",
  "description": "Daily service health segments with uptime percentage, accessible labels and popover details.",
  "dependencies": [],
  "registryDependencies": [
    "badge",
    "popover",
    "utils",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/uptime-bar.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport { Badge } from \"@/registry/default/ui/badge\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/registry/default/ui/popover\"\nimport { cn } from \"@/registry/default/lib/utils\"\nimport {\n  calculateUptimePercentage,\n  formatDateLabel,\n  formatUptimePercentage,\n  getNextSegmentIndex,\n  getSegmentAccessibleLabel,\n  normalizeDayStatus,\n  STATUS_LABELS,\n  type UptimeDay,\n  type UptimeStatus,\n} from \"@/registry/default/lib/uptime-bar.ts\"\n\nconst STATUS_BG: Record<UptimeStatus, string> = {\n  operational: \"bg-chart-2\",\n  degraded: \"bg-chart-5\",\n  down: \"bg-destructive\",\n  \"no-data\": \"bg-muted\",\n}\n\nexport interface UptimeBarProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"defaultValue\" | \"title\"\n> {\n  days: UptimeDay[]\n  title?: React.ReactNode\n  showPercentage?: boolean\n  showRangeLabels?: boolean\n  startLabel?: React.ReactNode\n  endLabel?: React.ReactNode\n  degradedWeight?: number\n  decimals?: number\n  value?: number | null\n  defaultValue?: number | null\n  onValueChange?: (index: number | null) => void\n  renderDayContent?: (day: UptimeDay, index: number) => React.ReactNode\n}\n\nexport function UptimeBar({\n  days = [],\n  title,\n  showPercentage = true,\n  showRangeLabels = true,\n  startLabel,\n  endLabel,\n  degradedWeight = 0.5,\n  decimals = 2,\n  value,\n  defaultValue,\n  onValueChange,\n  renderDayContent,\n  className,\n  ...props\n}: UptimeBarProps) {\n  const { value: openIndex, setValue: setOpenIndex } = useControllableState<\n    number | null\n  >({\n    value,\n    defaultValue: defaultValue ?? null,\n    onChange: onValueChange,\n  })\n\n  const [focusedIndex, setFocusedIndex] = React.useState<number>(() => {\n    if (defaultValue !== undefined && defaultValue !== null) return defaultValue\n    return Math.max(0, days.length - 1)\n  })\n\n  const segmentRefs = React.useRef<(HTMLButtonElement | null)[]>([])\n\n  const uptimePercentage = React.useMemo(\n    () => calculateUptimePercentage(days, { degradedWeight, decimals }),\n    [days, degradedWeight, decimals]\n  )\n\n  const handleKeyDown = (\n    e: React.KeyboardEvent<HTMLButtonElement>,\n    currentIndex: number\n  ) => {\n    if ([\"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"].includes(e.key)) {\n      e.preventDefault()\n      const nextIndex = getNextSegmentIndex(currentIndex, e.key, days.length)\n      setFocusedIndex(nextIndex)\n      segmentRefs.current[nextIndex]?.focus()\n      if (openIndex !== null) {\n        setOpenIndex(nextIndex)\n      }\n    }\n  }\n\n  const activeTabIndex =\n    openIndex !== null && openIndex >= 0 && openIndex < days.length\n      ? openIndex\n      : Math.min(focusedIndex, Math.max(0, days.length - 1))\n\n  return (\n    <div\n      data-slot=\"uptime-bar\"\n      className={cn(\"w-full min-w-0 space-y-2\", className)}\n      {...props}\n    >\n      {(title !== undefined || showPercentage) && (\n        <div\n          data-slot=\"uptime-bar-header\"\n          className=\"flex items-center justify-between text-sm\"\n        >\n          {title !== undefined && (\n            <div\n              data-slot=\"uptime-bar-title\"\n              className=\"font-medium text-foreground\"\n            >\n              {title}\n            </div>\n          )}\n          {showPercentage && (\n            <div\n              data-slot=\"uptime-bar-percentage\"\n              className=\"ml-auto font-medium text-muted-foreground\"\n            >\n              {formatUptimePercentage(uptimePercentage, decimals)} uptime\n            </div>\n          )}\n        </div>\n      )}\n\n      <div\n        data-slot=\"uptime-bar-segments\"\n        role=\"group\"\n        aria-label={\n          typeof title === \"string\"\n            ? `${title} uptime history`\n            : \"Uptime history\"\n        }\n        className=\"flex w-full min-w-0 items-center gap-[min(0.125rem,0.5%)]\"\n      >\n        {days.map((day, index) => {\n          const status = normalizeDayStatus(day.status)\n          const accessibleLabel = getSegmentAccessibleLabel(day)\n          const isOpen = openIndex === index\n\n          return (\n            <Popover\n              key={index}\n              open={isOpen}\n              onOpenChange={(nextOpen) => {\n                setOpenIndex(nextOpen ? index : null)\n              }}\n            >\n              <PopoverTrigger\n                ref={(el: HTMLButtonElement | null) => {\n                  segmentRefs.current[index] = el\n                }}\n                type=\"button\"\n                tabIndex={activeTabIndex === index ? 0 : -1}\n                data-slot=\"uptime-bar-segment\"\n                data-status={status}\n                aria-label={accessibleLabel}\n                title={accessibleLabel}\n                onFocus={() => setFocusedIndex(index)}\n                onKeyDown={(e) => handleKeyDown(e, index)}\n                className={cn(\n                  \"relative h-8 min-w-0 flex-1 rounded-xs transition-opacity hover:opacity-80 focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-hidden\",\n                  STATUS_BG[status],\n                  activeTabIndex === index && \"ring-1 ring-ring/30\"\n                )}\n              />\n              <PopoverContent\n                align=\"center\"\n                side=\"top\"\n                sideOffset={8}\n                className=\"w-64 p-3 text-xs\"\n              >\n                {renderDayContent ? (\n                  renderDayContent(day, index)\n                ) : (\n                  <div className=\"space-y-2\">\n                    <div className=\"flex items-center justify-between gap-2\">\n                      <span className=\"font-semibold text-foreground\">\n                        {formatDateLabel(day.date, {\n                          weekday: \"short\",\n                          month: \"short\",\n                          day: \"numeric\",\n                          year: \"numeric\",\n                        })}\n                      </span>\n                      {typeof day.uptime === \"number\" && (\n                        <span className=\"font-mono text-muted-foreground\">\n                          {formatUptimePercentage(day.uptime, 1)}\n                        </span>\n                      )}\n                    </div>\n                    <div className=\"flex items-center gap-2\">\n                      <Badge\n                        variant=\"outline\"\n                        className={cn(\n                          \"gap-1.5 px-2 py-0.5 text-xs font-normal\",\n                          status === \"operational\" &&\n                            \"border-chart-2/30 bg-chart-2/10 text-chart-2\",\n                          status === \"degraded\" &&\n                            \"border-chart-5/30 bg-chart-5/10 text-chart-5\",\n                          status === \"down\" &&\n                            \"border-destructive/30 bg-destructive/10 text-destructive\",\n                          status === \"no-data\" &&\n                            \"border-border bg-muted text-muted-foreground\"\n                        )}\n                      >\n                        <span\n                          className={cn(\n                            \"size-1.5 rounded-full\",\n                            STATUS_BG[status]\n                          )}\n                        />\n                        {STATUS_LABELS[status]}\n                      </Badge>\n                    </div>\n                    {day.note ? (\n                      <p className=\"text-xs leading-relaxed text-muted-foreground\">\n                        {day.note}\n                      </p>\n                    ) : (\n                      <p className=\"text-xs text-muted-foreground\">\n                        {status === \"operational\"\n                          ? \"No downtime recorded on this day.\"\n                          : status === \"no-data\"\n                            ? \"No data recorded for this period.\"\n                            : \"Incident reported.\"}\n                      </p>\n                    )}\n                  </div>\n                )}\n              </PopoverContent>\n            </Popover>\n          )\n        })}\n      </div>\n\n      {showRangeLabels && days.length > 0 && (\n        <div\n          data-slot=\"uptime-bar-footer\"\n          className=\"flex items-center justify-between text-xs text-muted-foreground\"\n        >\n          <span data-slot=\"uptime-bar-start-label\">\n            {startLabel ?? `${days.length} days ago`}\n          </span>\n          <span data-slot=\"uptime-bar-end-label\">{endLabel ?? \"Today\"}</span>\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport interface UptimeBarLegendProps extends React.ComponentProps<\"div\"> {\n  statuses?: UptimeStatus[]\n}\n\nexport function UptimeBarLegend({\n  className,\n  statuses = [\"operational\", \"degraded\", \"down\", \"no-data\"],\n  ...props\n}: UptimeBarLegendProps) {\n  return (\n    <div\n      data-slot=\"uptime-bar-legend\"\n      className={cn(\n        \"flex flex-wrap items-center gap-4 text-xs text-muted-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {statuses.map((status) => (\n        <div key={status} className=\"flex items-center gap-1.5\">\n          <span className={cn(\"size-2 rounded-full\", STATUS_BG[status])} />\n          <span>{STATUS_LABELS[status]}</span>\n        </div>\n      ))}\n    </div>\n  )\n}\n\nexport { UptimeBar as default }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/uptime-bar.ts",
      "content": "export type UptimeStatus = \"operational\" | \"degraded\" | \"down\" | \"no-data\"\n\nexport interface UptimeDay {\n  date: string | Date\n  status: UptimeStatus | \"no data\"\n  uptime?: number\n  note?: string\n}\n\nexport const STATUS_LABELS: Record<UptimeStatus, string> = {\n  operational: \"Operational\",\n  degraded: \"Degraded performance\",\n  down: \"Major outage\",\n  \"no-data\": \"No data\",\n}\n\n/**\n * Normalizes user-provided status strings into canonical UptimeStatus values.\n */\nexport function normalizeDayStatus(status: string): UptimeStatus {\n  const s = status\n    .toLowerCase()\n    .trim()\n    .replace(/[\\s_]+/g, \"-\")\n  if (s === \"operational\") return \"operational\"\n  if (s === \"degraded\") return \"degraded\"\n  if (s === \"down\" || s === \"outage\") return \"down\"\n  if (s === \"no-data\" || s === \"nodata\") return \"no-data\"\n  return \"no-data\"\n}\n\n/**\n * Calculates overall uptime percentage over a list of days.\n * Days with 'no-data' do not count against uptime.\n */\nexport function calculateUptimePercentage(\n  days: UptimeDay[],\n  options?: { degradedWeight?: number; decimals?: number }\n): number {\n  const degradedWeight = options?.degradedWeight ?? 0.5\n  const decimals = options?.decimals ?? 2\n\n  const eligibleDays = days.filter(\n    (day) => normalizeDayStatus(day.status) !== \"no-data\"\n  )\n\n  if (eligibleDays.length === 0) {\n    return 100\n  }\n\n  let totalScore = 0\n  for (const day of eligibleDays) {\n    if (typeof day.uptime === \"number\" && !Number.isNaN(day.uptime)) {\n      totalScore += Math.max(0, Math.min(100, day.uptime)) / 100\n    } else {\n      const norm = normalizeDayStatus(day.status)\n      if (norm === \"operational\") {\n        totalScore += 1\n      } else if (norm === \"degraded\") {\n        totalScore += degradedWeight\n      } else {\n        totalScore += 0\n      }\n    }\n  }\n\n  const percentage = (totalScore / eligibleDays.length) * 100\n  const factor = Math.pow(10, decimals)\n  return Math.round(percentage * factor) / factor\n}\n\n/**\n * Formats uptime percentage into human-readable string.\n */\nexport function formatUptimePercentage(\n  percentage: number,\n  decimals = 2\n): string {\n  return `${percentage.toFixed(decimals)}%`\n}\n\n/**\n * Formats a date into a localized string.\n */\nexport function formatDateLabel(\n  date: string | Date,\n  options: Intl.DateTimeFormatOptions = {\n    month: \"short\",\n    day: \"numeric\",\n    year: \"numeric\",\n  }\n): string {\n  const dateOnly =\n    typeof date === \"string\" && /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(date)\n  let d: Date\n  if (dateOnly) {\n    const [year, month, day] = dateOnly.slice(1).map(Number)\n    d = new Date(year, month - 1, day)\n    d.setFullYear(year)\n    if (d.getMonth() !== month - 1 || d.getDate() !== day) {\n      return String(date)\n    }\n  } else {\n    d = typeof date === \"string\" ? new Date(date) : date\n  }\n  if (Number.isNaN(d.getTime())) {\n    return String(date)\n  }\n  return new Intl.DateTimeFormat(\"en-US\", options).format(d)\n}\n\n/**\n * Generates an accessible aria-label for an individual uptime segment.\n */\nexport function getSegmentAccessibleLabel(day: UptimeDay): string {\n  const dateStr = formatDateLabel(day.date, {\n    month: \"long\",\n    day: \"numeric\",\n    year: \"numeric\",\n  })\n  const status = normalizeDayStatus(day.status)\n  const statusText = STATUS_LABELS[status].toLowerCase()\n  const noteText = day.note ? `: ${day.note}` : \"\"\n  return `${dateStr}, ${statusText}${noteText}`\n}\n\n/**\n * Calculates next roving segment index based on keyboard arrow keys.\n */\nexport function getNextSegmentIndex(\n  currentIndex: number,\n  key: string,\n  totalCount: number\n): number {\n  if (totalCount <= 0) return 0\n  switch (key) {\n    case \"ArrowLeft\":\n      return Math.max(0, currentIndex - 1)\n    case \"ArrowRight\":\n      return Math.min(totalCount - 1, currentIndex + 1)\n    case \"Home\":\n      return 0\n    case \"End\":\n      return totalCount - 1\n    default:\n      return currentIndex\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}