{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "latency-histogram",
  "type": "registry:component",
  "title": "Latency Histogram",
  "description": "Request latency as a histogram with p50, p95 and p99 marked on it; drag a brush across the bars (or Shift-arrow) to see how many requests fall in any window.",
  "categories": [
    "component",
    "Numbers"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/tabs.json"
  ],
  "files": [
    {
      "path": "registry/devl/latency-histogram.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { XIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type KeyboardEvent, type PointerEvent, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Tabs, TabsList, TabsTab } from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface LatencyHistogramProps {\n  title?: string;\n  caption?: string;\n  bins?: number[];\n  binSize?: number;\n  percentiles?: number[];\n  defaultRange?: [number, number] | null;\n  onRangeChange?: (range: [number, number] | null) => void;\n  defaultScale?: HistogramScale;\n  locale?: string;\n  className?: string;\n}\n\nexport type HistogramScale = \"linear\" | \"log\";\n\nconst H = 168;\nconst TOP = 30;\nconst BOTTOM = 22;\nconst MARKER = [\n  { line: \"stroke-muted-foreground\", chip: \"bg-muted text-foreground\", dot: \"bg-muted-foreground\" },\n  { line: \"stroke-warning\", chip: \"bg-warning/12 text-warning-foreground dark:bg-warning/16\", dot: \"bg-warning\" },\n  { line: \"stroke-destructive\", chip: \"bg-destructive/10 text-destructive-foreground dark:bg-destructive/16\", dot: \"bg-destructive\" },\n];\n\nfunction mulberry(seed: number) {\n  return () => {\n    seed |= 0;\n    seed = (seed + 0x6d2b79f5) | 0;\n    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nfunction sampleBins(count = 50, size = 30) {\n  const rand = mulberry(9);\n  const logn = (x: number, mu: number, sigma: number) =>\n    Math.exp(-((Math.log(x) - mu) ** 2) / (2 * sigma * sigma)) / (x * sigma * Math.sqrt(2 * Math.PI));\n  return Array.from({ length: count }, (_, i) => {\n    const x = (i + 0.5) * size;\n    const density = 0.9 * logn(x, Math.log(190), 0.42) + 0.1 * logn(x, Math.log(880), 0.18);\n    return Math.round(48000 * density * size * (0.9 + rand() * 0.2));\n  });\n}\n\nconst defaultBins = sampleBins();\n\nexport function LatencyHistogram({\n  title = \"Request latency\",\n  caption = \"POST /v1/checkout · last hour\",\n  bins = defaultBins,\n  binSize = 30,\n  percentiles = [0.5, 0.95, 0.99],\n  defaultRange = null,\n  onRangeChange,\n  defaultScale = \"linear\",\n  locale = \"en-US\",\n  className,\n}: LatencyHistogramProps) {\n  const reduce = useReducedMotion();\n  const wrapRef = useRef<HTMLDivElement>(null);\n  const [width, setWidth] = useState(440);\n  const [hover, setHover] = useState<number | null>(null);\n  const [range, setRange] = useState<[number, number] | null>(() =>\n    defaultRange ? [Math.floor(defaultRange[0] / binSize), Math.ceil(defaultRange[1] / binSize) - 1] : null,\n  );\n  const drag = useRef<{ mode: \"new\" | \"lo\" | \"hi\" | \"move\"; anchor: number; origin: [number, number] } | null>(null);\n  const [dragging, setDragging] = useState(false);\n  const [scale, setScale] = useState<HistogramScale>(defaultScale);\n  const num = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n\n  useEffect(() => {\n    const el = wrapRef.current;\n    if (!el) return;\n    const ro = new ResizeObserver(([entry]) => setWidth(Math.max(240, entry.contentRect.width)));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  const n = bins.length;\n  const total = bins.reduce((a, b) => a + b, 0);\n  const peak = Math.max(...bins, 1);\n  const pitch = width / n;\n  const barW = Math.max(1, pitch - 2);\n  const plotH = H - TOP - BOTTOM;\n  const xOf = (ms: number) => (ms / (n * binSize)) * width;\n  const ms = (v: number) => (v >= 1000 ? `${Number((v / 1000).toFixed(2))}s` : `${Math.round(v)}ms`);\n\n  const marks = useMemo(() => {\n    let acc = 0;\n    const cum = bins.map((b) => (acc += b));\n    return percentiles.map((p) => {\n      const goal = p * total;\n      const i = cum.findIndex((c) => c >= goal);\n      const before = i > 0 ? cum[i - 1] : 0;\n      const within = bins[i] ? (goal - before) / bins[i] : 0;\n      return { p, value: (i + within) * binSize };\n    });\n  }, [bins, percentiles, total, binSize]);\n\n  const sel = range ? [Math.min(range[0], range[1]), Math.max(range[0], range[1])] : null;\n  const selCount = sel ? bins.slice(sel[0], sel[1] + 1).reduce((a, b) => a + b, 0) : 0;\n\n  function commit(next: [number, number] | null) {\n    setRange(next);\n    if (onRangeChange) {\n      if (!next) onRangeChange(null);\n      else {\n        const lo = Math.min(next[0], next[1]);\n        const hi = Math.max(next[0], next[1]);\n        onRangeChange([lo * binSize, (hi + 1) * binSize]);\n      }\n    }\n  }\n\n  const binAt = (clientX: number) => {\n    const r = wrapRef.current!.getBoundingClientRect();\n    return Math.max(0, Math.min(n - 1, Math.floor(((clientX - r.left) / r.width) * n)));\n  };\n\n  function onPointerDown(e: PointerEvent<HTMLDivElement>) {\n    if (e.button !== 0) return;\n    const i = binAt(e.clientX);\n    const target = (e.target as Element).closest(\"[data-handle]\")?.getAttribute(\"data-handle\");\n    e.currentTarget.setPointerCapture(e.pointerId);\n    if (sel && target === \"lo\") drag.current = { mode: \"lo\", anchor: sel[1], origin: [sel[0], sel[1]] };\n    else if (sel && target === \"hi\") drag.current = { mode: \"hi\", anchor: sel[0], origin: [sel[0], sel[1]] };\n    else if (sel && i >= sel[0] && i <= sel[1] && target === \"body\") drag.current = { mode: \"move\", anchor: i, origin: [sel[0], sel[1]] };\n    else {\n      drag.current = { mode: \"new\", anchor: i, origin: [i, i] };\n      setRange([i, i]);\n    }\n    setDragging(true);\n  }\n\n  function onPointerMove(e: PointerEvent<HTMLDivElement>) {\n    const i = binAt(e.clientX);\n    setHover(i);\n    const d = drag.current;\n    if (!d) return;\n    if (d.mode === \"move\") {\n      const span = d.origin[1] - d.origin[0];\n      const lo = Math.max(0, Math.min(n - 1 - span, d.origin[0] + i - d.anchor));\n      setRange([lo, lo + span]);\n    } else setRange([d.anchor, i]);\n  }\n\n  function onPointerUp() {\n    const d = drag.current;\n    drag.current = null;\n    setDragging(false);\n    if (!d) return;\n    if (d.mode === \"new\" && range && range[0] === range[1]) {\n      commit(null);\n      return;\n    }\n    commit(range);\n  }\n\n  function onKeyDown(e: KeyboardEvent<HTMLDivElement>) {\n    const cur = hover ?? Math.round(marks[0]?.value / binSize) ?? 0;\n    if (e.key === \"Escape\") {\n      commit(null);\n      return;\n    }\n    const step = e.key === \"ArrowRight\" ? 1 : e.key === \"ArrowLeft\" ? -1 : 0;\n    if (!step && e.key !== \"Home\" && e.key !== \"End\") return;\n    e.preventDefault();\n    const next = e.key === \"Home\" ? 0 : e.key === \"End\" ? n - 1 : Math.max(0, Math.min(n - 1, cur + step));\n    if (e.shiftKey) {\n      const anchor = range ? range[0] : cur;\n      commit([anchor, next]);\n    }\n    setHover(next);\n  }\n\n  const yOf = (v: number) =>\n    scale === \"log\" ? Math.log10(1 + v) / Math.log10(1 + peak) : v / peak;\n  const mag = 10 ** Math.floor(Math.log10(peak / 2));\n  const gridStep = [1, 2, 5, 10].map((k) => k * mag).find((v) => peak / v <= 3.2) ?? mag * 10;\n  const grid = (\n    scale === \"log\"\n      ? [10, 100, 1000, 10000, 100000]\n      : Array.from({ length: Math.floor(peak / gridStep) }, (_, k) => (k + 1) * gridStep)\n  ).filter((g) => g < peak * 0.96);\n  const tickStep = width < 380 ? 500 : 250;\n  const ticks = Array.from({ length: Math.floor((n * binSize) / tickStep) + 1 }, (_, i) => i * tickStep);\n  const markRows = marks.map((m, i) => {\n    const prev = marks[i - 1];\n    return prev && xOf(m.value) - xOf(prev.value) < 78 ? 1 : 0;\n  });\n  for (let i = 1; i < markRows.length; i++) if (markRows[i] === 1 && markRows[i - 1] === 1) markRows[i] = 0;\n\n  const tipBin = hover !== null && !dragging ? hover : null;\n  const tipX = tipBin === null ? 0 : tipBin * pitch + pitch / 2;\n  const tipAlign = tipX < 80 ? \"start\" : tipX > width - 80 ? \"end\" : \"center\";\n\n  return (\n    <section\n      aria-label={title}\n      className={cn(\"w-full max-w-[520px] rounded-2xl border bg-card p-5 text-card-foreground shadow-xs/5\", className)}\n    >\n      <header className=\"flex items-start justify-between gap-3\">\n        <div className=\"flex min-w-0 flex-col gap-0.5\">\n          <h3 className=\"font-medium text-sm\">{title}</h3>\n          <p className=\"text-pretty text-muted-foreground text-xs\">\n            <span className=\"tabular-nums\">{num.format(total)}</span> requests · {caption}\n          </p>\n        </div>\n        <Tabs onValueChange={(v) => setScale(v as HistogramScale)} value={scale}>\n          <TabsList aria-label=\"Count scale\" className=\"text-muted-foreground\">\n            <TabsTab className=\"px-2 text-xs sm:text-xs\" value=\"linear\">\n              Linear\n            </TabsTab>\n            <TabsTab className=\"px-2 text-xs sm:text-xs\" value=\"log\">\n              Log\n            </TabsTab>\n          </TabsList>\n        </Tabs>\n      </header>\n\n      <dl className=\"mt-4 grid grid-cols-3 gap-2\">\n        {marks.map((m, i) => (\n          <div className=\"flex flex-col gap-0.5 rounded-xl bg-muted/60 px-3 py-2 dark:bg-muted\" key={m.p}>\n            <dt className=\"flex items-center gap-1.5 text-muted-foreground text-xs\">\n              <span className={cn(\"h-2.5 w-0.5 rounded-full\", MARKER[i % 3].dot)} />p{Math.round(m.p * 100)}\n            </dt>\n            <dd className=\"font-semibold text-base tabular-nums tracking-tight\">{ms(m.value)}</dd>\n          </div>\n        ))}\n      </dl>\n\n      <div\n        aria-label={`${title} histogram. Arrow keys move, Shift plus arrows select a range, Escape clears.`}\n        className={cn(\n          \"relative mt-4 touch-none select-none rounded-md outline-none ring-offset-4 ring-offset-card focus-visible:ring-2 focus-visible:ring-ring\",\n          dragging ? \"cursor-ew-resize\" : \"cursor-crosshair\",\n        )}\n        onBlur={() => setHover(null)}\n        onKeyDown={onKeyDown}\n        onPointerDown={onPointerDown}\n        onPointerLeave={() => !drag.current && setHover(null)}\n        onPointerMove={onPointerMove}\n        onPointerUp={onPointerUp}\n        ref={wrapRef}\n        role=\"application\"\n        tabIndex={0}\n      >\n        <svg aria-hidden className=\"block overflow-visible\" height={H} width={width}>\n          {grid.map((g) => {\n            const y = TOP + plotH - yOf(g) * plotH;\n            return (\n              <g key={g}>\n                <line className=\"stroke-border\" strokeDasharray=\"2 4\" x1={0} x2={width} y1={y} y2={y} />\n                <text\n                  className=\"fill-muted-foreground stroke-card text-[10px] tabular-nums\"\n                  strokeLinejoin=\"round\"\n                  strokeWidth={3}\n                  style={{ paintOrder: \"stroke\" }}\n                  textAnchor=\"end\"\n                  x={width}\n                  y={y - 3}\n                >\n                  {g >= 1000 ? `${g / 1000}k` : g}\n                </text>\n              </g>\n            );\n          })}\n          <line className=\"stroke-border\" x1={0} x2={width} y1={TOP + plotH} y2={TOP + plotH} />\n          {bins.map((b, i) => {\n            const h = Math.round(Math.max(b ? 2 : 0, yOf(b) * plotH) * 10) / 10;\n            const inSel = sel ? i >= sel[0] && i <= sel[1] : true;\n            const hot = hover === i && !dragging;\n            return (\n              <motion.rect\n                animate={{ scaleY: 1, height: h, attrY: TOP + plotH - h }}\n                className={cn(\n                  sel ? (inSel ? \"fill-info\" : \"fill-foreground/12\") : hot ? \"fill-foreground/70\" : \"fill-foreground/35\",\n                  sel && inSel && hot && \"fill-info/80\",\n                )}\n                initial={{ scaleY: 0, height: h, attrY: TOP + plotH - h }}\n                key={i}\n                rx={Math.min(2, barW / 2)}\n                style={{ originY: 1, transformBox: \"fill-box\" }}\n                transition={\n                  reduce\n                    ? { duration: 0 }\n                    : {\n                        scaleY: { type: \"spring\", stiffness: 260, damping: 30, delay: Math.min(0.35, i * 0.006) },\n                        default: { type: \"spring\", stiffness: 380, damping: 38 },\n                      }\n                }\n                width={barW}\n                x={i * pitch + 1}\n              />\n            );\n          })}\n\n          {sel ? (\n            <g>\n              <rect\n                className=\"fill-info/8 stroke-info/40\"\n                data-handle=\"body\"\n                height={plotH + 6}\n                rx={4}\n                strokeDasharray=\"3 3\"\n                width={(sel[1] - sel[0] + 1) * pitch}\n                x={sel[0] * pitch}\n                y={TOP - 3}\n              />\n              {([\"lo\", \"hi\"] as const).map((h) => {\n                const x = h === \"lo\" ? sel[0] * pitch : (sel[1] + 1) * pitch;\n                return (\n                  <g className=\"cursor-ew-resize\" data-handle={h} key={h}>\n                    <rect className=\"fill-transparent\" height={plotH + 6} width={24} x={x - 12} y={TOP - 3} />\n                    <rect className=\"fill-info\" height={plotH + 6} rx={1} width={2} x={x - 1} y={TOP - 3} />\n                    <rect className=\"fill-card stroke-info\" height={18} rx={3} strokeWidth={1.5} width={8} x={x - 4} y={TOP + plotH / 2 - 9} />\n                  </g>\n                );\n              })}\n            </g>\n          ) : null}\n\n          {marks.map((m, i) => {\n            const x = xOf(m.value);\n            return (\n              <line\n                className={cn(\"pointer-events-none\", MARKER[i % 3].line)}\n                key={m.p}\n                strokeDasharray=\"2 3\"\n                strokeWidth={1.25}\n                x1={x}\n                x2={x}\n                y1={markRows[i] ? 28 : 16}\n                y2={TOP + plotH}\n              />\n            );\n          })}\n\n          {ticks.map((t, k) => (\n            <text\n              className=\"fill-muted-foreground text-[10px] tabular-nums\"\n              key={t}\n              textAnchor={k === 0 ? \"start\" : t === n * binSize ? \"end\" : \"middle\"}\n              x={xOf(t)}\n              y={H - 4}\n            >\n              {ms(t)}\n            </text>\n          ))}\n        </svg>\n\n        {marks.map((m, i) => (\n          <span\n            className={cn(\n              \"pointer-events-none absolute -translate-x-1/2 rounded-full px-1.5 font-medium text-[10px] tabular-nums leading-4\",\n              MARKER[i % 3].chip,\n            )}\n            key={m.p}\n            style={{ left: Math.max(24, Math.min(width - 24, xOf(m.value))), top: markRows[i] ? 12 : 0 }}\n          >\n            p{Math.round(m.p * 100)}\n          </span>\n        ))}\n\n        <AnimatePresence>\n          {tipBin !== null ? (\n            <motion.div\n              animate={{ opacity: 1 }}\n              className={cn(\n                \"pointer-events-none absolute z-10 whitespace-nowrap rounded-lg border bg-popover px-2.5 py-1.5 text-popover-foreground text-xs shadow-md/5\",\n                tipAlign === \"center\" && \"-translate-x-1/2\",\n                tipAlign === \"end\" && \"-translate-x-full\",\n              )}\n              exit={{ opacity: 0, transition: { duration: 0.08 } }}\n              initial={{ opacity: 0 }}\n              style={{ left: tipAlign === \"start\" ? tipX - pitch / 2 : tipAlign === \"end\" ? tipX + pitch / 2 : tipX, top: TOP + 4 }}\n              transition={{ duration: reduce ? 0 : 0.12 }}\n            >\n              <span className=\"text-muted-foreground tabular-nums\">\n                {ms(tipBin * binSize)}–{ms((tipBin + 1) * binSize)}\n              </span>{\" \"}\n              <span className=\"font-medium tabular-nums\">{num.format(bins[tipBin])}</span>\n              <span className=\"text-muted-foreground\"> req</span>\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n      </div>\n\n      <div className=\"mt-3 flex h-7 items-center justify-between gap-3 text-xs\">\n        <AnimatePresence initial={false} mode=\"wait\">\n          {sel && sel[1] > sel[0] ? (\n            <motion.p\n              animate={{ opacity: 1, y: 0 }}\n              className=\"min-w-0 truncate tabular-nums\"\n              exit={{ opacity: 0, y: reduce ? 0 : -3 }}\n              initial={{ opacity: 0, y: reduce ? 0 : 3 }}\n              key=\"sel\"\n              transition={{ duration: 0.15 }}\n            >\n              <span className=\"font-medium\">\n                {ms(sel[0] * binSize)}–{ms((sel[1] + 1) * binSize)}\n              </span>\n              <span className=\"text-muted-foreground\">\n                {\" \"}\n                · {num.format(selCount)} requests · {((selCount / (total || 1)) * 100).toFixed(1)}%\n              </span>\n            </motion.p>\n          ) : (\n            <motion.p\n              animate={{ opacity: 1, y: 0 }}\n              className=\"truncate text-muted-foreground\"\n              exit={{ opacity: 0, y: reduce ? 0 : -3 }}\n              initial={{ opacity: 0, y: reduce ? 0 : 3 }}\n              key=\"hint\"\n              transition={{ duration: 0.15 }}\n            >\n              Drag across the chart to select a range\n            </motion.p>\n          )}\n        </AnimatePresence>\n        {sel && sel[1] > sel[0] ? (\n          <button\n            className=\"inline-flex h-7 shrink-0 cursor-pointer items-center gap-1 rounded-md px-2 text-muted-foreground outline-none transition-[color,background-color,scale] duration-150 ease-out hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]\"\n            onClick={() => commit(null)}\n            type=\"button\"\n          >\n            <XIcon className=\"size-3.5\" />\n            Clear\n          </button>\n        ) : null}\n      </div>\n    </section>\n  );\n}\n"
    }
  ]
}