{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "uptime-bar",
  "title": "uptime-bar",
  "description": "A strip of uptime periods with a downtime slice per tier (the theme's status scale) and an optional lag cap, read with the pointer or the arrow keys, with one tooltip of per-period detail.",
  "dependencies": [
    "class-variance-authority@^0.7"
  ],
  "registryDependencies": [
    "@dwellir/dwellir-theme",
    "@dwellir/utils",
    "@dwellir/tooltip"
  ],
  "files": [
    {
      "path": "components/ui/uptime-bar.tsx",
      "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport {\n  type ComponentProps,\n  type CSSProperties,\n  type KeyboardEvent,\n  lazy,\n  type ReactNode,\n  type PointerEvent as ReactPointerEvent,\n  type RefObject,\n  Suspense,\n  useEffect,\n  useRef,\n  useState,\n} from 'react';\nimport { cn } from '@/lib/utils';\n\ntype UptimeTier = 'operational' | 'minor' | 'major' | 'down' | 'no-data';\n\n/** The lowest uptime, in %, of each tier: a period below `major` is down. */\ntype UptimeThresholds = { clean: number; minor: number; major: number };\n\n/*\n * Clean from 99.99%: a day below it lost more than 8.6 s. A fleet-wide day\n * always loses a few seconds, so a rule of any downtime slices every period.\n */\nconst UPTIME_THRESHOLDS: UptimeThresholds = { clean: 99.99, minor: 95, major: 80 };\n\n/* A slice under 5% of the bar is too thin to see. The ui-uptime-bar utility draws 5% unless told more. */\nconst MIN_SLICE_PERCENT = 5;\n\n/* The tooltip loads on the first reading, so a strip without details ships none of it. */\nconst UptimeBarTooltip = lazy(() => import('@/components/ui/uptime-bar-tooltip'));\n\n/**\n * One period of the strip, oldest first. Give each period its own label.\n * `details` are lines under the label and uptime in the tooltip, such as\n * \"Downtime: 4 hours\"; the slider reads them too. `marker` draws a cap on top\n * of the bar: `lag` is for a period that lagged behind the chain head. The cap\n * is decorative, so also say the fact in `details`.\n */\ntype UptimePeriod = {\n  label: string;\n  uptime: number | null;\n  details?: string[];\n  marker?: 'lag';\n};\n\n/** The tier of an uptime in %, for a bar's slice, a badge or a figure's ink. */\nfunction uptimeTier(\n  uptime: number | null,\n  thresholds: UptimeThresholds = UPTIME_THRESHOLDS,\n): UptimeTier {\n  if (uptime === null) return 'no-data';\n  if (uptime >= thresholds.clean) return 'operational';\n  if (uptime >= thresholds.minor) return 'minor';\n  if (uptime >= thresholds.major) return 'major';\n  return 'down';\n}\n\nconst formatUptime = (uptime: number | null) =>\n  uptime === null ? 'no data' : `${uptime.toFixed(2)}%`;\n\n/* What the slider announces: the label and uptime, then each detail line. */\nconst spoken = (period: UptimePeriod) =>\n  [`${period.label}: ${formatUptime(period.uptime)}`, ...(period.details ?? [])].join('. ');\n\nconst uptimeBarVariants = cva(\n  'ui-uptime-bar flex cursor-crosshair flex-col justify-end gap-2 rounded-sm outline-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring focus-visible:outline-solid',\n  {\n    variants: {\n      /* default: 32 px bars in a row. fill: the bars fill a positioned parent. */\n      size: { default: '', fill: 'absolute inset-0' },\n    },\n    defaultVariants: { size: 'default' },\n  },\n);\n\nconst trackVariants = cva('flex', {\n  variants: {\n    /* The gap is in ui-uptime-bar, which also places the tooltip's anchor from it. */\n    size: { default: 'h-8', fill: 'min-h-0 flex-1' },\n    /* A tooltip anchors to a bar inside the track. */\n    anchored: { true: 'relative', false: '' },\n  },\n  defaultVariants: { size: 'default', anchored: false },\n});\n\ntype UptimeBarProps = Omit<ComponentProps<'div'>, 'children'> &\n  VariantProps<typeof uptimeBarVariants> & {\n    /** Names the strip for screen readers, such as \"Daily uptime, past 90 days\". */\n    'aria-label': string;\n    periods: UptimePeriod[];\n    /** Overrides the tier cuts (UPTIME_THRESHOLDS) one by one. */\n    thresholds?: Partial<UptimeThresholds>;\n    /** The readout's ends while no period is read: the first and the last label by default. */\n    start?: ReactNode;\n    end?: ReactNode;\n    /**\n     * The line under the bars that reads the period under the pointer or the\n     * arrow keys. A strip with details has a tooltip that says the same, so it\n     * may set this to false.\n     */\n    readout?: boolean;\n  };\n\n/* The downtime slice's height, when it is above the utility's 5% minimum. */\nfunction sliceHeight(uptime: number | null, tier: UptimeTier) {\n  if (uptime === null || tier === 'operational' || tier === 'no-data') return undefined;\n  const percent = Math.min(100, 100 - uptime);\n  return percent > MIN_SLICE_PERCENT ? `${Number(percent.toFixed(1))}%` : undefined;\n}\n\n/** The period the keys move to, or undefined for a key the strip ignores. */\nfunction nextPeriod(key: string, current: number | null, last: number) {\n  const move = {\n    ArrowLeft: -1,\n    ArrowDown: -1,\n    ArrowRight: 1,\n    ArrowUp: 1,\n    Home: -last,\n    End: last,\n  }[key];\n  if (move === undefined) return undefined;\n  return Math.min(last, Math.max(0, (current ?? last) + move));\n}\n\nfunction Readout({\n  read,\n  start,\n  end,\n}: {\n  read: UptimePeriod | null;\n  start: ReactNode;\n  end: ReactNode;\n}) {\n  return (\n    <div\n      data-slot=\"uptime-bar-readout\"\n      aria-hidden=\"true\"\n      className=\"flex justify-between font-mono text-xs text-muted-foreground\"\n    >\n      <span data-slot=\"uptime-bar-label\">{read ? read.label : start}</span>\n      <span data-slot=\"uptime-bar-value\" className={read ? 'text-foreground' : undefined}>\n        {read ? formatUptime(read.uptime) : end}\n      </span>\n    </div>\n  );\n}\n\n/*\n * A touch tap keeps its reading after the finger lifts (the pointer leave of a\n * touch is ignored), so a tap outside the strip ends it. A blur does not\n * always follow a tap on a page that is not focusable, as on iOS.\n */\nfunction useDismissOutside(\n  track: RefObject<HTMLDivElement | null>,\n  on: boolean,\n  dismiss: () => void,\n) {\n  useEffect(() => {\n    if (!on) return;\n    const onDown = (event: PointerEvent) => {\n      const strip = track.current?.parentElement;\n      if (strip && event.target instanceof Node && !strip.contains(event.target)) dismiss();\n    };\n    document.addEventListener('pointerdown', onDown);\n    return () => document.removeEventListener('pointerdown', onDown);\n  }, [track, on, dismiss]);\n}\n\n/* One bare bar: the ui-uptime-bar utility draws it, as classes on 90 periods would bloat the HTML. */\nfunction PeriodBar({\n  period,\n  tier,\n  active,\n  latest,\n}: {\n  period: UptimePeriod;\n  tier: UptimeTier;\n  active: boolean;\n  latest: boolean;\n}) {\n  const slice = sliceHeight(period.uptime, tier);\n  return (\n    <i\n      data-slot=\"uptime-bar-period\"\n      data-tier={tier === 'operational' ? undefined : tier}\n      data-active={active ? '' : undefined}\n      data-current={latest ? '' : undefined}\n      data-marker={period.marker}\n      style={slice ? ({ '--uptime-slice': slice } as CSSProperties) : undefined}\n    />\n  );\n}\n\nfunction DetailTip({\n  track,\n  index,\n  count,\n  period,\n  open,\n  onDismiss,\n}: {\n  track: RefObject<HTMLDivElement | null>;\n  index: number;\n  count: number;\n  period: UptimePeriod;\n  open: boolean;\n  onDismiss: () => void;\n}) {\n  return (\n    <Suspense fallback={null}>\n      <UptimeBarTooltip\n        track={track}\n        index={index}\n        count={count}\n        label={period.label}\n        uptime={period.uptime === null ? 'No data' : `Uptime: ${formatUptime(period.uptime)}`}\n        details={period.details ?? []}\n        open={open}\n        onDismiss={onDismiss}\n      />\n    </Suspense>\n  );\n}\n\n/* A series without periods: an image that reads \"no data\". */\nfunction EmptyStrip({\n  size,\n  readout,\n  start,\n  end,\n  ...props\n}: ComponentProps<'div'> & Pick<UptimeBarProps, 'size' | 'readout' | 'start' | 'end'>) {\n  return (\n    <div {...props} role=\"img\" aria-label={`${props['aria-label']}: no data`}>\n      <div data-slot=\"uptime-bar-track\" aria-hidden=\"true\" className={trackVariants({ size })} />\n      {readout ? <Readout read={null} start={start} end={end ?? 'no data'} /> : null}\n    </div>\n  );\n}\n\n/**\n * A strip of uptime periods, oldest first: a green bar per period, with a\n * slice at its foot in the tier's colour when the period lost time. The bars\n * are decorative. The strip is one slider: the pointer and the arrow keys\n * (Home and End too) read a period, and the slider announces its date and\n * uptime. Without periods, the strip is an image that reads \"no data\".\n * When a period has details, one tooltip follows the active period and the\n * slider reads the details after the uptime.\n */\nfunction UptimeBar({\n  className,\n  size,\n  periods,\n  thresholds,\n  start,\n  end,\n  readout = true,\n  ...props\n}: UptimeBarProps) {\n  const [active, setActive] = useState<number | null>(null);\n  const [dismissed, setDismissed] = useState(false);\n  const track = useRef<HTMLDivElement>(null);\n  const last = periods.length - 1;\n  useDismissOutside(track, active !== null, () => {\n    setActive(null);\n    setDismissed(false);\n  });\n  const root = {\n    'data-slot': 'uptime-bar',\n    'data-size': size ?? 'default',\n    className: cn(uptimeBarVariants({ size }), className),\n  };\n  if (last < 0) {\n    return (\n      <EmptyStrip {...props} {...root} size={size} readout={readout} start={start} end={end} />\n    );\n  }\n  /* A series that shrinks while a period is read keeps the reading in range. */\n  const current = active === null ? null : Math.min(active, last);\n  const cuts = { ...UPTIME_THRESHOLDS, ...thresholds };\n  const read = periods[current ?? last];\n  const detailed = periods.some((period) => period.details?.length);\n  /* A new reading shows the tooltip again after Escape hid it. */\n  const reading = (next: number | null) => {\n    setActive(next);\n    setDismissed(false);\n  };\n  /*\n   * A press reads the bar it lands on. A drag keeps the first bar as its\n   * target (a touch captures it), so a move reads the period under its x: a\n   * finger dragging across the strip (touch-action pan-y) keeps reading, and\n   * the bars need no handler each.\n   */\n  const pressed = (event: ReactPointerEvent<HTMLDivElement>) =>\n    Array.prototype.indexOf.call(event.currentTarget.children, event.target);\n  const under = (event: ReactPointerEvent<HTMLDivElement>) => {\n    const box = event.currentTarget.getBoundingClientRect();\n    const at = Math.floor(((event.clientX - box.left) / box.width) * periods.length);\n    return Math.max(0, Math.min(last, at));\n  };\n  const step = (event: KeyboardEvent<HTMLDivElement>) => {\n    const next = nextPeriod(event.key, current, last);\n    if (next === undefined) return;\n    event.preventDefault();\n    reading(next);\n  };\n  return (\n    <div\n      {...props}\n      {...root}\n      data-reading={current === null ? undefined : ''}\n      role=\"slider\"\n      aria-valuemin={0}\n      aria-valuemax={last}\n      aria-valuenow={current ?? last}\n      aria-valuetext={spoken(read)}\n      tabIndex={0}\n      onKeyDown={step}\n      onFocus={() => setActive((value) => value ?? last)}\n      onBlur={() => reading(null)}\n      onPointerLeave={(event) => {\n        if (event.pointerType !== 'touch') reading(null);\n      }}\n    >\n      <div\n        ref={track}\n        data-slot=\"uptime-bar-track\"\n        aria-hidden=\"true\"\n        className={trackVariants({ size, anchored: detailed })}\n        onPointerDown={(event) => {\n          const bar = pressed(event);\n          reading(bar >= 0 && bar <= last ? bar : under(event));\n        }}\n        onPointerMove={(event) => {\n          const next = under(event);\n          if (next !== current) reading(next);\n        }}\n      >\n        {/* The ui-uptime-bar utility draws each period: classes on 90 periods would bloat the HTML. */}\n        {periods.map((period, index) => (\n          <PeriodBar\n            key={period.label}\n            period={period}\n            tier={uptimeTier(period.uptime, cuts)}\n            active={index === current}\n            latest={index === last}\n          />\n        ))}\n        {detailed && current !== null ? (\n          <DetailTip\n            track={track}\n            index={current}\n            count={periods.length}\n            period={read}\n            open={!dismissed}\n            onDismiss={() => setDismissed(true)}\n          />\n        ) : null}\n      </div>\n      {readout ? (\n        <Readout\n          read={current === null ? null : periods[current]}\n          start={start ?? periods[0].label}\n          end={end ?? periods[last].label}\n        />\n      ) : null}\n    </div>\n  );\n}\n\nexport {\n  UPTIME_THRESHOLDS,\n  UptimeBar,\n  type UptimePeriod,\n  type UptimeThresholds,\n  type UptimeTier,\n  uptimeBarVariants,\n  uptimeTier,\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "components/ui/uptime-bar-tooltip.tsx",
      "content": "'use client';\n\nimport type { CSSProperties, RefObject } from 'react';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\n\ntype PeriodTooltipProps = {\n  /** The strip's track: the tooltip anchors to the child at `index`. */\n  track: RefObject<HTMLDivElement | null>;\n  index: number;\n  /** The number of periods: a strip that gains or loses bars moves them. */\n  count: number;\n  label: string;\n  /** The uptime line, such as \"Uptime: 99.00%\", or \"No data\". */\n  uptime: string;\n  details: string[];\n  open: boolean;\n  onDismiss: () => void;\n};\n\n/*\n * The strip's one tooltip. A trigger-less anchor sits over the active bar, so\n * 90 bars mount one tooltip, not 90. The part loads this file on the first\n * reading of a strip with details, so other strips ship none of it.\n */\nexport default function UptimeBarTooltip({\n  track,\n  index,\n  count,\n  label,\n  uptime,\n  details,\n  open,\n  onDismiss,\n}: PeriodTooltipProps) {\n  return (\n    <Tooltip open={open} onOpenChange={(next) => !next && onDismiss()}>\n      <TooltipTrigger asChild>\n        <span\n          data-slot=\"uptime-bar-anchor\"\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-y-0\"\n          /* ui-uptime-bar places it from the index and the count: no layout read per bar. */\n          style={\n            {\n              '--uptime-index': Math.min(index, count - 1),\n              '--uptime-count': count,\n            } as CSSProperties\n          }\n        />\n      </TooltipTrigger>\n      <TooltipContent\n        size=\"sm\"\n        className=\"max-w-xs tabular-nums\"\n        collisionPadding={12}\n        /* A tap or a click on the strip reads a bar; it does not close the tooltip. */\n        onPointerDownOutside={(event) => {\n          if (event.target instanceof Node && track.current?.contains(event.target)) {\n            event.preventDefault();\n          }\n        }}\n      >\n        <p className=\"font-medium\">{label}</p>\n        <p className=\"text-background-80\">{uptime}</p>\n        {details.map((line) => (\n          <p key={line} className=\"text-background-80\">\n            {line}\n          </p>\n        ))}\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}