{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "line-chart",
  "type": "registry:ui",
  "title": "Line Chart",
  "description": "Multi-series lines with a snapping crosshair, full readout and a toggling legend.",
  "categories": [
    "data"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/line-chart.tsx",
      "content": "\"use client\";\nimport { animate, AnimatePresence, motion, useInView, useMotionValue, useReducedMotion, useTransform } from \"motion/react\";\nimport { useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\nimport { ease, spring } from \"@/lib/motion\";\n\nconst noop = () => () => {};\nfunction useLocale(locale?: string) {\n  const detected = useSyncExternalStore(noop, () => Intl.NumberFormat().resolvedOptions().locale, () => \"en-US\");\n  return locale ?? detected;\n}\n\nfunction useWidth() {\n  const [node, setNode] = useState<HTMLElement | null>(null);\n  const [width, setWidth] = useState(0);\n  useEffect(() => {\n    if (!node) return;\n    const ro = new ResizeObserver(([e]) => setWidth(Math.round(e.contentRect.width)));\n    ro.observe(node);\n    return () => ro.disconnect();\n  }, [node]);\n  return [setNode, width] as const;\n}\n\n/** Round ticks covering [min, max]. Lines don't need a zero baseline unless asked for. */\nfunction niceTicks(min: number, max: number, count: number, zero: boolean) {\n  let lo0 = zero ? Math.min(0, min) : min;\n  let hi0 = zero ? Math.max(0, max) : max;\n  if (hi0 === lo0) [lo0, hi0] = [lo0 - 1, hi0 + 1];\n  const raw = (hi0 - lo0) / count;\n  const mag = 10 ** Math.floor(Math.log10(raw));\n  const n = raw / mag;\n  const step = (n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10) * mag;\n  const lo = Math.floor(lo0 / step) * step;\n  const hi = Math.ceil(hi0 / step) * step;\n  const out: number[] = [];\n  for (let v = lo; v <= hi + step / 2; v += step) out.push(Math.round(v / step) * step);\n  return out;\n}\n\ntype Pt = { x: number; y: number };\n\n// Monotone cubic (Fritsch–Carlson): smooth, but never overshoots a peak or invents a dip.\nfunction monotone(pts: Pt[]) {\n  const n = pts.length;\n  if (n < 3) return pts.map((p, i) => `${i ? \"L\" : \"M\"}${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(\"\");\n  const dx: number[] = [];\n  const m: number[] = [];\n  for (let i = 0; i < n - 1; i++) {\n    dx.push(pts[i + 1].x - pts[i].x);\n    m.push((pts[i + 1].y - pts[i].y) / (dx[i] || 1));\n  }\n  const t = [m[0]];\n  for (let i = 1; i < n - 1; i++) t.push(m[i - 1] * m[i] <= 0 ? 0 : (3 * (dx[i - 1] + dx[i])) / ((2 * dx[i] + dx[i - 1]) / m[i - 1] + (dx[i] + 2 * dx[i - 1]) / m[i]));\n  t.push(m[n - 2]);\n  let d = `M${pts[0].x.toFixed(2)} ${pts[0].y.toFixed(2)}`;\n  for (let i = 0; i < n - 1; i++) {\n    const h = dx[i] / 3;\n    d += `C${(pts[i].x + h).toFixed(2)} ${(pts[i].y + h * t[i]).toFixed(2)} ${(pts[i + 1].x - h).toFixed(2)} ${(pts[i + 1].y - h * t[i + 1]).toFixed(2)} ${pts[i + 1].x.toFixed(2)} ${pts[i + 1].y.toFixed(2)}`;\n  }\n  return d;\n}\n\nfunction linePath(pts: (Pt | null)[], curve: \"linear\" | \"monotone\") {\n  const runs: Pt[][] = [[]];\n  for (const p of pts) {\n    if (p) runs[runs.length - 1].push(p);\n    else if (runs[runs.length - 1].length) runs.push([]);\n  }\n  return runs\n    .filter((r) => r.length)\n    .map((r) => (curve === \"monotone\" ? monotone(r) : r.map((p, i) => `${i ? \"L\" : \"M\"}${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(\"\")))\n    .join(\"\");\n}\n\nconst inks = [0.95, 0.5, 0.3, 0.18];\n\nexport type LineSeries = {\n  key: string;\n  label: string;\n  /** Text-color class for this series, e.g. \"text-info\". Defaults to the foreground at a stepped opacity. */\n  className?: string;\n  /** Dashed reads as \"for comparison\": last period, a forecast, a budget. */\n  variant?: \"solid\" | \"dashed\";\n};\n\nexport type LineChartProps<T extends Record<string, unknown>> = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  data: T[];\n  /** The field on the x axis: a date label, a week, a timestamp string. */\n  xKey: keyof T & string;\n  series: LineSeries[];\n  /** Names the chart for screen readers. */\n  label: string;\n  /** Plot height in pixels. */\n  height?: number;\n  curve?: \"linear\" | \"monotone\";\n  /** A soft wash under the first visible series. */\n  area?: boolean;\n  /** Keep zero on the y axis, for quantities where the distance from zero matters. */\n  zero?: boolean;\n  formatValue?: (value: number) => string;\n  formatX?: (x: string) => string;\n  locale?: string;\n  /** Series keys hidden by the legend (controlled). */\n  hidden?: string[];\n  defaultHidden?: string[];\n  onHiddenChange?: (hidden: string[]) => void;\n  loading?: boolean;\n  emptyLabel?: string;\n  /** Draw the lines in the first time the chart scrolls into view. */\n  animated?: boolean;\n};\n\nexport function LineChart<T extends Record<string, unknown>>({\n  data,\n  xKey,\n  series,\n  label,\n  height = 220,\n  curve = \"linear\",\n  area = false,\n  zero = false,\n  formatValue,\n  formatX = (x) => x,\n  locale: localeProp,\n  hidden: hiddenProp,\n  defaultHidden = [],\n  onHiddenChange,\n  loading = false,\n  emptyLabel = \"No data for this period\",\n  animated = true,\n  className,\n  ...rest\n}: LineChartProps<T>) {\n  const locale = useLocale(localeProp);\n  const reduce = useReducedMotion();\n  const [measure, width] = useWidth();\n  const root = useRef<HTMLDivElement>(null);\n  const inView = useInView(root, { once: true, amount: 0.35 });\n  const [hidden, setHidden] = useControllableState({ value: hiddenProp, defaultValue: defaultHidden, onChange: onHiddenChange });\n  const [index, setIndex] = useState<number | null>(null);\n  const [emphasis, setEmphasis] = useState<string | null>(null);\n  const [keyboard, setKeyboard] = useState(false);\n  const gradient = useId();\n  const descId = useId();\n\n  const fmt = useMemo(() => {\n    const nf = new Intl.NumberFormat(locale, { notation: \"compact\", maximumFractionDigits: 1 });\n    return formatValue ?? ((v: number) => nf.format(v));\n  }, [formatValue, locale]);\n\n  const visible = series.filter((s) => !hidden.includes(s.key));\n  const num = (d: T, key: string) => {\n    const v = d[key];\n    if (v == null || v === \"\") return null;\n    const n = Number(v);\n    return Number.isFinite(n) ? n : null;\n  };\n  // The y range follows the visible series, so hiding a tall line lets the rest fill the plot.\n  const vals = data.flatMap((d) => visible.map((s) => num(d, s.key))).filter((v): v is number => v != null);\n  const ticks = niceTicks(vals.length ? Math.min(...vals) : 0, vals.length ? Math.max(...vals) : 1, 4, zero);\n  const t0 = ticks[0];\n  const t1 = ticks[ticks.length - 1];\n\n  const tickWidth = Math.max(...ticks.map((t) => fmt(t).length)) * 6.4 + 10;\n  const m = { top: 10, right: 12, bottom: 26, left: tickWidth };\n  const plotW = Math.max(0, width - m.left - m.right);\n  const n = data.length;\n  const xAt = (i: number) => m.left + (n <= 1 ? plotW / 2 : (i / (n - 1)) * plotW);\n  const yAt = (v: number) => m.top + height - ((v - t0) / (t1 - t0 || 1)) * height;\n  const totalH = height + m.top + m.bottom;\n\n  const paths = series.map((s) => {\n    const pts = data.map((d, i) => {\n      const v = num(d, s.key);\n      return v == null ? null : { x: xAt(i), y: yAt(v) };\n    });\n    return { key: s.key, pts, d: linePath(pts, curve) };\n  });\n  const firstVisible = paths.find((p) => !hidden.includes(p.key));\n  const areaD =\n    area && firstVisible\n      ? (() => {\n          const run = firstVisible.pts.filter((p): p is Pt => !!p);\n          if (run.length < 2) return \"\";\n          const floor = m.top + height;\n          return `${linePath(firstVisible.pts, curve)}L${run[run.length - 1].x.toFixed(2)} ${floor}L${run[0].x.toFixed(2)} ${floor}Z`;\n        })()\n      : \"\";\n\n  // X labels: as many as fit without touching, always including the last.\n  const xLabel = (i: number) => formatX(String(data[i][xKey]));\n  const longest = Math.max(4, ...data.map((_, i) => xLabel(i).length));\n  const fit = Math.max(2, Math.floor(plotW / (longest * 6.4 + 24)));\n  const stepX = Math.max(1, Math.ceil((n - 1) / (fit - 1)));\n  const xTicks = n ? Array.from(new Set([...Array.from({ length: Math.floor((n - 1) / stepX) + 1 }, (_, k) => k * stepX)])) : [];\n  if (xTicks.length && xTicks[xTicks.length - 1] !== n - 1) {\n    // Swap the last regular tick for the final point when they'd collide.\n    if ((n - 1 - xTicks[xTicks.length - 1]) * (plotW / Math.max(1, n - 1)) < longest * 6.4 + 12) xTicks.pop();\n    xTicks.push(n - 1);\n  }\n\n  const empty = !loading && n === 0;\n  const skeleton = loading && n === 0;\n  const allHidden = n > 0 && visible.length === 0;\n  const drawn = !animated || inView;\n  const [settled, setSettled] = useState(!animated);\n  useEffect(() => {\n    if (!drawn || settled) return;\n    const t = window.setTimeout(() => setSettled(true), 1100);\n    return () => window.clearTimeout(t);\n  }, [drawn, settled]);\n\n  // The crosshair: one x, sprung after the pointer, jumped for the keyboard.\n  const cx = useMotionValue(0);\n  const side = useMotionValue(0);\n  const tipX = useTransform([cx, side], ([x, s]: number[]) => `calc(${x}px + ${s < 0.5 ? 12 : -12}px - ${s * 100}%)`);\n  const scrubbing = index != null && !empty && !skeleton && !allHidden;\n  const wasOn = useRef(false);\n  const targetX = index == null ? null : xAt(index);\n  const flip = targetX != null && targetX > m.left + plotW / 2 ? 1 : 0;\n  useEffect(() => {\n    if (targetX == null) {\n      wasOn.current = false;\n      return;\n    }\n    // The tooltip sits right of the crosshair in the left half and left of it in the right half.\n    side.jump(flip);\n    if (!wasOn.current || reduce || keyboard) cx.jump(targetX);\n    else animate(cx, targetX, spring.follow);\n    wasOn.current = true;\n  }, [targetX, flip, reduce, keyboard, cx, side]);\n\n  const nearest = (e: React.PointerEvent) => {\n    const r = e.currentTarget.getBoundingClientRect();\n    const ratio = (e.clientX - r.left - m.left) / (plotW || 1);\n    return Math.min(n - 1, Math.max(0, Math.round(ratio * (n - 1))));\n  };\n\n  const toggle = (key: string) => {\n    const hiding = !hidden.includes(key);\n    setHidden(hiding ? [...hidden, key] : hidden.filter((k) => k !== key));\n    // Showing a series again emphasizes it, so the eye finds where it came back; hiding lets go.\n    setEmphasis(hiding ? null : key);\n  };\n  const inkOf = (si: number) => (series[si].className ? 1 : inks[Math.min(si, inks.length - 1)]);\n  const readout = (i: number) =>\n    `${xLabel(i)}: ${visible\n      .map((s) => {\n        const v = num(data[i], s.key);\n        return `${s.label} ${v == null ? \"no data\" : fmt(v)}`;\n      })\n      .join(\", \")}`;\n  const current = index ?? (n ? n - 1 : 0);\n\n  return (\n    <div\n      ref={root}\n      data-slot=\"line-chart\"\n      data-state={skeleton ? \"loading\" : empty ? \"empty\" : loading ? \"refreshing\" : \"ready\"}\n      aria-busy={loading || undefined}\n      className={cn(\"flex w-full min-w-0 flex-col gap-3 text-fg\", className)}\n      {...rest}\n    >\n      {series.length > 1 && (\n        <div role=\"group\" aria-label=\"Series\" className=\"-ml-1.5 flex flex-wrap items-center gap-0.5\">\n          {series.map((s, si) => {\n            const on = !hidden.includes(s.key);\n            return (\n              <button\n                key={s.key}\n                type=\"button\"\n                aria-pressed={on}\n                onClick={() => toggle(s.key)}\n                onPointerEnter={(e) => e.pointerType === \"mouse\" && on && setEmphasis(s.key)}\n                onPointerLeave={() => setEmphasis(null)}\n                onFocus={() => on && setEmphasis(s.key)}\n                onBlur={() => setEmphasis(null)}\n                className={cn(\n                  \"group/key relative flex h-7 select-none items-center gap-1.5 rounded-md px-1.5 text-[12px] outline-none\",\n                  \"transition-[background-color,color,scale] duration-150 hover:bg-hover active:scale-[0.97]\",\n                  \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n                  \"before:absolute before:-inset-x-0.5 before:-inset-y-2 before:content-[''] pointer-fine:before:hidden\",\n                  on ? \"text-fg-2 hover:text-fg\" : \"text-fg-4 hover:text-fg-3\",\n                )}\n              >\n                <svg width=\"14\" height=\"8\" aria-hidden className={cn(\"shrink-0 transition-opacity duration-200\", s.className ?? \"text-fg\")} style={{ opacity: on ? inkOf(si) : 0.25 }}>\n                  <line x1=\"1\" x2=\"13\" y1=\"4\" y2=\"4\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeDasharray={s.variant === \"dashed\" ? \"3 3\" : undefined} />\n                </svg>\n                <span className={cn(\"transition-[text-decoration-color] duration-200\", !on && \"line-through decoration-fg-4\")}>{s.label}</span>\n              </button>\n            );\n          })}\n        </div>\n      )}\n\n      <div\n        ref={measure}\n        role={n > 0 && !allHidden ? \"slider\" : \"img\"}\n        aria-label={label}\n        aria-describedby={descId}\n        aria-valuemin={n > 0 && !allHidden ? 0 : undefined}\n        aria-valuemax={n > 0 && !allHidden ? n - 1 : undefined}\n        aria-valuenow={n > 0 && !allHidden ? current : undefined}\n        aria-valuetext={n > 0 && !allHidden ? readout(current) : undefined}\n        tabIndex={n > 0 && !allHidden ? 0 : undefined}\n        className=\"relative w-full cursor-crosshair touch-pan-y select-none rounded-md outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-4 focus-visible:outline-fg-3\"\n        style={{ height: totalH }}\n        onPointerMove={(e) => {\n          if (!n || allHidden || (e.pointerType !== \"mouse\" && e.buttons === 0)) return;\n          setKeyboard(false);\n          setIndex(nearest(e));\n        }}\n        onPointerDown={(e) => {\n          if (!n || allHidden) return;\n          setKeyboard(false);\n          setIndex(nearest(e));\n        }}\n        onPointerUp={(e) => e.pointerType !== \"mouse\" && setIndex(null)}\n        onPointerLeave={() => setIndex(null)}\n        onPointerCancel={() => setIndex(null)}\n        onFocus={(e) => {\n          if (n && !allHidden && e.currentTarget.matches(\":focus-visible\")) {\n            setKeyboard(true);\n            setIndex(n - 1);\n          }\n        }}\n        onBlur={() => setIndex(null)}\n        onKeyDown={(e) => {\n          if (!n || allHidden) return;\n          const from = index ?? n - 1;\n          const to =\n            e.key === \"ArrowLeft\" || e.key === \"ArrowDown\"\n              ? Math.max(0, from - 1)\n              : e.key === \"ArrowRight\" || e.key === \"ArrowUp\"\n                ? Math.min(n - 1, from + 1)\n                : e.key === \"Home\"\n                  ? 0\n                  : e.key === \"End\"\n                    ? n - 1\n                    : e.key === \"PageDown\"\n                      ? Math.max(0, from - 7)\n                      : e.key === \"PageUp\"\n                        ? Math.min(n - 1, from + 7)\n                        : undefined;\n          if (e.key === \"Escape\" && index != null) {\n            e.preventDefault();\n            setIndex(null);\n            return;\n          }\n          if (to == null) return;\n          e.preventDefault();\n          setKeyboard(true);\n          setIndex(to);\n        }}\n      >\n        <span id={descId} className=\"sr-only\">\n          {n ? `${n} points from ${xLabel(0)} to ${xLabel(n - 1)}. Use the arrow keys to read each point.` : emptyLabel}\n        </span>\n        {width > 0 && (\n          <>\n            <svg width={width} height={totalH} aria-hidden className={cn(\"block overflow-visible transition-opacity duration-200\", loading && n > 0 && \"opacity-50\")}>\n              <defs>\n                <linearGradient id={gradient} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n                  <stop offset=\"0\" stopColor=\"currentColor\" stopOpacity={0.12} />\n                  <stop offset=\"1\" stopColor=\"currentColor\" stopOpacity={0} />\n                </linearGradient>\n              </defs>\n              <AnimatePresence initial={false}>\n                {ticks.map((t) => {\n                  const y = Math.round(yAt(t)) + 0.5;\n                  return (\n                    <motion.line\n                      key={t}\n                      x1={m.left}\n                      x2={width - m.right}\n                      className={t === 0 ? \"stroke-line-2\" : \"stroke-line\"}\n                      strokeWidth={1}\n                      initial={{ opacity: 0, y1: y, y2: y }}\n                      animate={{ opacity: 1, y1: y, y2: y }}\n                      exit={{ opacity: 0, transition: { duration: 0.12 } }}\n                      transition={{ duration: reduce ? 0 : 0.45, ease: ease.inOut }}\n                    />\n                  );\n                })}\n              </AnimatePresence>\n\n              {skeleton && (\n                <path\n                  d={monotone(Array.from({ length: 8 }, (_, i) => ({ x: m.left + (i / 7) * plotW, y: m.top + height * (0.35 + 0.3 * Math.sin(i * 1.1)) })))}\n                  fill=\"none\"\n                  strokeWidth={2}\n                  className=\"stroke-fg/[0.08] motion-safe:animate-pulse-soft\"\n                />\n              )}\n\n              {areaD && firstVisible && (\n                <motion.path\n                  className={series.find((s) => s.key === firstVisible.key)?.className ?? \"text-fg\"}\n                  fill={`url(#${gradient})`}\n                  initial={false}\n                  animate={{ d: areaD, opacity: drawn ? (emphasis && emphasis !== firstVisible.key && !hidden.includes(emphasis) ? 0.3 : 1) : 0 }}\n                  transition={{ d: { duration: reduce ? 0 : 0.45, ease: ease.inOut }, opacity: { duration: 0.4, delay: !settled && !reduce ? 0.5 : 0 } }}\n                />\n              )}\n\n              {paths.map((p, si) => {\n                const s = series[si];\n                const on = !hidden.includes(p.key);\n                const dim = emphasis != null && emphasis !== p.key && !hidden.includes(emphasis);\n                const dashed = s.variant === \"dashed\";\n                return (\n                  <motion.path\n                    key={p.key}\n                    className={s.className ?? \"text-fg\"}\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth={2}\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeDasharray={dashed ? \"4 4\" : undefined}\n                    initial={false}\n                    // Solid lines draw along their length; a dashed comparison line fades in once they have.\n                    animate={{\n                      d: p.d,\n                      ...(dashed ? null : { pathLength: drawn ? 1 : 0 }),\n                      opacity: !on || (dashed && !drawn) ? 0 : dim ? inkOf(si) * 0.25 : inkOf(si),\n                    }}\n                    transition={{\n                      d: { duration: reduce ? 0 : 0.45, ease: ease.inOut },\n                      pathLength: { duration: reduce ? 0 : 1, ease: ease.outQuart, delay: reduce ? 0 : si * 0.08 },\n                      opacity: { duration: reduce ? 0.15 : dashed && !settled ? 0.4 : 0.2, delay: dashed && !settled && !reduce ? 0.6 : 0 },\n                    }}\n                    style={{ pointerEvents: \"none\" }}\n                  />\n                );\n              })}\n\n              {/* Crosshair and a ringed dot where it crosses each visible line. */}\n              <motion.g initial={false} animate={{ opacity: scrubbing ? 1 : 0 }} transition={{ duration: scrubbing ? 0.1 : 0.12 }}>\n                <motion.line y1={m.top} y2={m.top + height} x1={cx} x2={cx} className=\"stroke-fg/25\" strokeWidth={1} />\n                {scrubbing &&\n                  index != null &&\n                  paths.map((p, si) => {\n                    const pt = p.pts[index];\n                    if (!pt || hidden.includes(p.key)) return null;\n                    return (\n                      <motion.circle\n                        key={p.key}\n                        r={4}\n                        cx={cx}\n                        initial={false}\n                        animate={{ cy: pt.y }}\n                        transition={reduce || keyboard ? { duration: 0 } : spring.follow}\n                        className={cn(series[si].className ?? \"text-fg\", \"stroke-raised\")}\n                        fill=\"currentColor\"\n                        fillOpacity={Math.max(inkOf(si), 0.5)}\n                        strokeWidth={2}\n                      />\n                    );\n                  })}\n              </motion.g>\n            </svg>\n\n            <div aria-hidden className=\"pointer-events-none absolute inset-0 text-2xs text-fg-3 tabular\">\n              <AnimatePresence initial={false}>\n                {n > 0 &&\n                  !allHidden &&\n                  ticks.map((t) => (\n                    <motion.span\n                      key={t}\n                      className=\"absolute left-0 -translate-y-1/2 whitespace-nowrap pr-2 text-right\"\n                      style={{ width: m.left }}\n                      initial={{ opacity: 0, top: yAt(t) }}\n                      animate={{ opacity: 1, top: yAt(t) }}\n                      exit={{ opacity: 0, transition: { duration: 0.12 } }}\n                      transition={{ duration: reduce ? 0 : 0.45, ease: ease.inOut }}\n                    >\n                      {fmt(t)}\n                    </motion.span>\n                  ))}\n              </AnimatePresence>\n              {xTicks.map((i) => (\n                <span\n                  key={i}\n                  className={cn(\n                    \"absolute whitespace-nowrap text-[11px] transition-colors duration-150\",\n                    i === 0 && n > 1 ? \"\" : i === n - 1 && n > 1 ? \"-translate-x-full\" : \"-translate-x-1/2\",\n                    index === i ? \"text-fg\" : \"text-fg-3\",\n                  )}\n                  style={{ left: xAt(i), top: m.top + height + 8 }}\n                >\n                  {xLabel(i)}\n                </span>\n              ))}\n            </div>\n\n            {(empty || allHidden) && (\n              <div className=\"absolute grid place-items-center\" style={{ left: m.left, top: m.top, width: plotW, height }}>\n                {empty ? (\n                  <span role=\"status\" className=\"text-[12.5px] text-fg-3\">\n                    {emptyLabel}\n                  </span>\n                ) : (\n                  <div className=\"flex flex-col items-center gap-2\">\n                    <span className=\"text-[12.5px] text-fg-3\">All series are hidden</span>\n                    <button\n                      type=\"button\"\n                      onClick={() => setHidden([])}\n                      className=\"h-7 rounded-md border border-line-2 bg-raised px-2.5 text-[12px] font-medium text-fg shadow-[var(--shadow)] outline-none transition-[background-color,border-color,scale] duration-150 hover:border-fg-4 hover:bg-hover focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3 active:scale-[0.97]\"\n                    >\n                      Show all\n                    </button>\n                  </div>\n                )}\n              </div>\n            )}\n\n            <AnimatePresence>\n              {scrubbing && index != null && (\n                <motion.div\n                  key=\"tip\"\n                  className=\"pointer-events-none absolute left-0 z-(--z-tooltip)\"\n                  style={{ top: m.top, x: tipX }}\n                  initial={{ opacity: 0, scale: reduce ? 1 : 0.96 }}\n                  animate={{ opacity: 1, scale: 1 }}\n                  exit={{ opacity: 0, transition: { duration: 0.1 } }}\n                  transition={{ duration: 0.14, ease: ease.out }}\n                >\n                  <div className=\"min-w-[136px] rounded-lg border border-line-2 bg-raised px-2.5 py-2 shadow-pop\">\n                    <div className=\"mb-1.5 whitespace-nowrap text-[11.5px] text-fg-2\">{xLabel(index)}</div>\n                    <div className=\"flex flex-col gap-1\">\n                      {series.map((s, si) => {\n                        if (hidden.includes(s.key)) return null;\n                        const v = num(data[index], s.key);\n                        return (\n                          <div key={s.key} className=\"flex items-center gap-2 whitespace-nowrap text-[12px]\">\n                            <svg width=\"10\" height=\"4\" aria-hidden className={cn(\"shrink-0\", s.className ?? \"text-fg\")} style={{ opacity: Math.max(inkOf(si), 0.45) }}>\n                              <line x1=\"1\" x2=\"9\" y1=\"2\" y2=\"2\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeDasharray={s.variant === \"dashed\" ? \"2 2\" : undefined} />\n                            </svg>\n                            <span className=\"min-w-0 flex-1 text-fg-2\">{s.label}</span>\n                            <span className=\"pl-3 font-medium text-fg tabular\">{v == null ? \"–\" : fmt(v)}</span>\n                          </div>\n                        );\n                      })}\n                    </div>\n                  </div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/line-chart"
}
