{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "traffic-chart",
  "title": "Traffic chart",
  "description": "Requests and 5xx errors over time with deploys marked as dashed rules, a range switch (hour, day, week), the range's totals and a legend that names every line, so colour is never the only key.",
  "registryDependencies": [
    "https://sproutboat.com/ikada/r/theme.json",
    "https://sproutboat.com/ikada/r/chart-theme.json",
    "https://sproutboat.com/ikada/r/cn.json",
    "https://sproutboat.com/ikada/r/format.json",
    "https://sproutboat.com/ikada/r/segmented-control.json",
    "https://sproutboat.com/ikada/r/timeseries-chart.json"
  ],
  "files": [
    {
      "path": "registry/ikada/blocks/traffic-chart/traffic-chart.tsx",
      "content": "\"use client\";\n\n/**\n * Requests and 5xx errors over time with deploys marked as dashed rules, a range switch (hour,\n * day, week), the range's totals and a legend that names every line, so colour is never the only key.\n */\nimport { useMemo, useState } from \"react\";\nimport { chartColors } from \"@/components/ikada/chart-theme\";\nimport type { ChartColorName } from \"@/components/ikada/chart-theme\";\nimport { TimeseriesChart } from \"@/components/ikada/timeseries-chart\";\nimport type { DeployMarker, TimeseriesPoint } from \"@/components/ikada/timeseries-chart\";\nimport { SegmentedControl } from \"@/components/ikada/segmented-control\";\nimport { cn } from \"@/components/ikada/lib/cn\";\nimport { formatCompact } from \"@/components/ikada/lib/format\";\n\nexport type TrafficRange = \"1h\" | \"24h\" | \"7d\";\n\nexport interface TrafficData {\n  requests: TimeseriesPoint[];\n  errors: TimeseriesPoint[];\n  deploys: DeployMarker[];\n}\n\nexport interface TrafficChartProps {\n  /** The data for a range. Defaults to generated sample traffic. */\n  data?: (range: TrafficRange) => TrafficData;\n  /** The range shown first. */\n  defaultRange?: TrafficRange;\n  onRangeChange?: (range: TrafficRange) => void;\n  title?: string;\n  className?: string;\n}\n\nconst RANGES = [\n  { value: \"1h\", label: \"1 hour\" },\n  { value: \"24h\", label: \"24 hours\" },\n  { value: \"7d\", label: \"7 days\" },\n] as const satisfies readonly { value: TrafficRange; label: string }[];\n\nconst RANGE_TEXT: Record<TrafficRange, string> = { \"1h\": \"last hour\", \"24h\": \"last 24 hours\", \"7d\": \"last 7 days\" };\n\n/** Deterministic noise, so the sample looks real and renders the same every time. */\nfunction seeded(seed: number): () => number {\n  let s = seed;\n  return () => {\n    s = (s + 0x6d2b79f5) | 0;\n    let t = Math.imul(s ^ (s >>> 15), 1 | s);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nconst MINUTE = 60_000;\nconst HOUR = 60 * MINUTE;\n/** The sample's \"now\": a weekday afternoon. */\nconst NOW = Date.UTC(2026, 10, 5, 15, 0);\n\n/** Traffic on a day curve (busiest mid-afternoon UTC) with a burst of 5xx between a bad deploy and its fix. */\nfunction sample(range: TrafficRange): TrafficData {\n  const spec = {\n    \"1h\": { points: 60, step: MINUTE, perStep: 140 },\n    \"24h\": { points: 48, step: HOUR / 2, perStep: 4_200 },\n    \"7d\": { points: 56, step: 3 * HOUR, perStep: 24_000 },\n  }[range];\n  const rand = seeded(range.length * 97 + spec.points);\n  const start = NOW - (spec.points - 1) * spec.step;\n  const badDeploy = NOW - spec.step * Math.round(spec.points * 0.42);\n  const fixDeploy = badDeploy + spec.step * Math.max(3, Math.round(spec.points * 0.16));\n  const requests: TimeseriesPoint[] = [];\n  const errors: TimeseriesPoint[] = [];\n  for (let i = 0; i < spec.points; i++) {\n    const time = start + i * spec.step;\n    const hour = new Date(time).getUTCHours() + new Date(time).getUTCMinutes() / 60;\n    const curve = 0.25 + 0.75 * (Math.sin(((hour - 8) / 24) * Math.PI * 2) * 0.5 + 0.5) ** 1.4;\n    const weekend = range === \"7d\" && [0, 6].includes(new Date(time).getUTCDay()) ? 0.62 : 1;\n    const value = Math.round(spec.perStep * curve * weekend * (0.88 + rand() * 0.24));\n    const broken = time >= badDeploy && time < fixDeploy;\n    requests.push({ time, value });\n    errors.push({ time, value: Math.round(value * (broken ? 0.11 + rand() * 0.05 : 0.002 + rand() * 0.002)) });\n  }\n  return {\n    requests,\n    errors,\n    deploys: [\n      { time: badDeploy, label: range === \"7d\" ? \"v46\" : \"v47\" },\n      { time: fixDeploy, label: range === \"7d\" ? \"v47\" : \"v48\" },\n    ],\n  };\n}\n\nconst LEGEND: { label: string; color: ChartColorName; dashed?: boolean }[] = [\n  { label: \"Requests\", color: \"requests\" },\n  { label: \"Errors (5xx)\", color: \"errors\" },\n  { label: \"Deploy\", color: \"deploy\", dashed: true },\n];\n\nfunction Legend() {\n  return (\n    <ul className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-sb-sm text-fg-muted\">\n      {LEGEND.map((entry) => (\n        <li key={entry.label} className=\"inline-flex items-center gap-1.5\">\n          <svg aria-hidden=\"true\" width=\"16\" height=\"8\" viewBox=\"0 0 16 8\" className=\"shrink-0\">\n            <line\n              x1=\"1\"\n              y1=\"4\"\n              x2=\"15\"\n              y2=\"4\"\n              style={{ stroke: chartColors[entry.color] }}\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeDasharray={entry.dashed ? \"2 3\" : undefined}\n            />\n          </svg>\n          {entry.label}\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nexport function TrafficChart({\n  data = sample,\n  defaultRange = \"24h\",\n  onRangeChange,\n  title = \"Traffic\",\n  className,\n}: TrafficChartProps) {\n  const [range, setRange] = useState<TrafficRange>(defaultRange);\n  const traffic = useMemo(() => data(range), [data, range]);\n  const total = traffic.requests.reduce((sum, p) => sum + p.value, 0);\n  const failed = traffic.errors.reduce((sum, p) => sum + p.value, 0);\n  const rate = total ? (failed / total) * 100 : 0;\n  return (\n    <section\n      aria-labelledby=\"traffic-title\"\n      className={cn(\"@container flex flex-col gap-4 rounded-xl bg-surface p-4 text-fg shadow-card @lg:p-5\", className)}\n    >\n      <div className=\"flex flex-wrap items-start justify-between gap-x-4 gap-y-3\">\n        <div className=\"flex min-w-0 flex-col gap-0.5\">\n          <h2 id=\"traffic-title\" className=\"text-sb-md font-semibold\">\n            {title}\n          </h2>\n          <p className=\"text-sb-sm text-fg-muted\" aria-live=\"polite\">\n            <span className=\"font-semibold text-fg tabular-nums\">\n              {formatCompact(total, { maximumFractionDigits: 1 })}\n            </span>{\" \"}\n            requests ·{\" \"}\n            <span className={cn(\"font-semibold tabular-nums\", rate >= 1 ? \"text-danger-fg\" : \"text-fg\")}>\n              {rate.toFixed(2)}%\n            </span>{\" \"}\n            errors, {RANGE_TEXT[range]}\n          </p>\n        </div>\n        <SegmentedControl\n          aria-label=\"Time range\"\n          size=\"sm\"\n          value={range}\n          options={RANGES}\n          onValueChange={(next) => {\n            setRange(next);\n            onRangeChange?.(next);\n          }}\n          className=\"@md:w-72\"\n        />\n      </div>\n      <TimeseriesChart\n        aria-label={`Requests and 5xx errors, ${RANGE_TEXT[range]}`}\n        series={[\n          { id: \"requests\", label: \"Requests\", data: traffic.requests, area: true },\n          { id: \"errors\", label: \"Errors\", color: \"errors\", data: traffic.errors },\n        ]}\n        deploy={traffic.deploys}\n        height={220}\n      />\n      <Legend />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ikada/blocks/traffic-chart/traffic-chart.tsx"
    }
  ],
  "categories": [
    "dashboard"
  ],
  "type": "registry:block"
}