{
  "name": "metric-card",
  "type": "registry:component",
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Metric Card\n * @version: 2.0.0\n * @date: 2026-10-17\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport {\n  type KeyboardEvent,\n  type PointerEvent,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// The chart's drawing box; it stretches to the card's width.\nconst W = 400;\nconst H = 120;\nconst PAD_Y = 10;\n// Every period is resampled to this many points, so one shape can morph\n// into another.\nconst SAMPLES = 90;\nconst MORPH_MS = 420;\nconst DAY = 86_400_000;\nconst DIGIT = /\\d/;\nconst SNAP_PX = 6;\nconst DIGITS = [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"];\n\n// Swatches for the breakdown, darkest first.\nconst SWATCHES = [\n  \"bg-zinc-900 dark:bg-zinc-100\",\n  \"bg-zinc-400 dark:bg-zinc-500\",\n  \"bg-zinc-200 dark:bg-zinc-700\",\n  \"bg-zinc-300 dark:bg-zinc-600\",\n];\n\nexport interface MetricPoint {\n  /** YYYY-MM-DD */\n  date: string;\n  /** One value per segment, in the order of `segments`. */\n  parts: number[];\n}\n\nexport interface MetricEvent {\n  /** YYYY-MM-DD */\n  date: string;\n  label: string;\n}\n\nexport interface MetricCardProps {\n  label?: string;\n  /** Names of the parts that add up to the metric. */\n  segments?: string[];\n  /** Daily values, oldest first. Needs twice the longest period, so each period has one to compare with. */\n  data?: MetricPoint[];\n  /** Days worth a note, such as a launch or a price change. Pinned on the chart. */\n  events?: MetricEvent[];\n  periods?: number[];\n  defaultPeriod?: number;\n  format?: \"currency\" | \"number\";\n  currency?: string;\n  className?: string;\n}\n\ninterface Span {\n  lo: number;\n  hi: number;\n}\n\nconst clamp = (v: number, lo: number, hi: number) =>\n  Math.min(hi, Math.max(lo, v));\n\nconst sum = (values: number[]) => values.reduce((s, v) => s + v, 0);\n\n// Seeded, so server and browser draw the same chart.\nfunction demoData(days: number, end: string): MetricPoint[] {\n  let seed = 42;\n  const random = () => {\n    seed = (seed * 16_807) % 2_147_483_647;\n    return seed / 2_147_483_647;\n  };\n  const last = Date.parse(`${end}T00:00:00Z`);\n  const out: MetricPoint[] = [];\n  let plans = 820;\n  let usage = 380;\n  for (let i = 0; i < days; i++) {\n    const t = last - (days - 1 - i) * DAY;\n    const weekday = new Date(t).getUTCDay();\n    const weekend = weekday === 0 || weekday === 6;\n    plans += 1.6 + (random() - 0.5) * 9;\n    // New pricing lifts plans for good.\n    if (i === days - 12) {\n      plans *= 1.09;\n    }\n    usage += 0.9 + (random() - 0.5) * 14;\n    const promo = i >= days - 27 && i <= days - 22;\n    const services = 150 * (promo ? 2.1 : 1) * (0.7 + random() * 0.6);\n    out.push({\n      date: new Date(t).toISOString().slice(0, 10),\n      parts: [\n        Math.round(plans * (0.97 + random() * 0.06)),\n        Math.round(usage * (weekend ? 0.72 : 1) * (0.9 + random() * 0.2)),\n        Math.round(services * (weekend ? 0.5 : 1)),\n      ],\n    });\n  }\n  return out;\n}\n\nconst DEFAULT_DATA = demoData(180, \"2026-10-14\");\nconst DEFAULT_EVENTS: MetricEvent[] = [\n  { date: DEFAULT_DATA[180 - 27].date, label: \"Fall promo\" },\n  { date: DEFAULT_DATA[180 - 12].date, label: \"New pricing\" },\n];\n\nconst formatDay = (date: string) =>\n  new Date(`${date}T00:00:00Z`).toLocaleDateString(\"en-US\", {\n    weekday: \"short\",\n    month: \"short\",\n    day: \"numeric\",\n    timeZone: \"UTC\",\n  });\n\nconst formatShort = (date: string) =>\n  new Date(`${date}T00:00:00Z`).toLocaleDateString(\"en-US\", {\n    month: \"short\",\n    day: \"numeric\",\n    timeZone: \"UTC\",\n  });\n\n/** Linear resample of `values` to `count` evenly spaced points. */\nfunction resample(values: number[], count: number) {\n  // Too little history (no previous period yet): a flat line on the floor.\n  if (values.length === 0) {\n    return new Array<number>(count).fill(0);\n  }\n  const out = new Array<number>(count);\n  for (let i = 0; i < count; i++) {\n    const at = (i / (count - 1)) * (values.length - 1);\n    const lo = Math.floor(at);\n    const hi = Math.min(values.length - 1, lo + 1);\n    out[i] = values[lo] + (values[hi] - values[lo]) * (at - lo);\n  }\n  return out;\n}\n\nconst yOf = (height: number) => PAD_Y + (1 - height) * (H - PAD_Y * 2);\n\n/** Heights 0..1 to an SVG path, smoothed with a Catmull-Rom spline. */\nfunction toPath(heights: number[]) {\n  if (heights.length === 0) {\n    return \"\";\n  }\n  const pts = heights.map((h, i) => [\n    (i / Math.max(1, heights.length - 1)) * W,\n    yOf(h),\n  ]);\n  let d = `M${pts[0][0].toFixed(2)} ${pts[0][1].toFixed(2)}`;\n  for (let i = 0; i < pts.length - 1; i++) {\n    const p0 = pts[i - 1] ?? pts[i];\n    const p1 = pts[i];\n    const p2 = pts[i + 1];\n    const p3 = pts[i + 2] ?? p2;\n    const c1x = p1[0] + (p2[0] - p0[0]) / 6;\n    const c1y = p1[1] + (p2[1] - p0[1]) / 6;\n    const c2x = p2[0] - (p3[0] - p1[0]) / 6;\n    const c2y = p2[1] - (p3[1] - p1[1]) / 6;\n    d += `C${c1x.toFixed(2)} ${c1y.toFixed(2)} ${c2x.toFixed(2)} ${c2y.toFixed(2)} ${p2[0].toFixed(2)} ${p2[1].toFixed(2)}`;\n  }\n  return d;\n}\n\n/** The line closed down to the baseline. */\nconst toArea = (line: string) => `${line}L${W} ${H}L0 ${H}Z`;\n\ninterface Shape {\n  current: number[];\n  previous: number[];\n}\n\ninterface Paths {\n  current: string;\n  previous: string;\n}\n\n/** One period's numbers and its shape, for the whole metric or one segment. */\nfunction buildPeriod(data: MetricPoint[], days: number, focus: number | null) {\n  const current = data.slice(-days);\n  const previous = data.slice(-days * 2, -days);\n  const pick = (p: MetricPoint) =>\n    focus === null ? sum(p.parts) : (p.parts[focus] ?? 0);\n  const now = current.map(pick);\n  const before = previous.map(pick);\n  const all = [...now, ...before];\n  const lo = Math.min(...all) * 0.85;\n  const hi = Math.max(...all) * 1.04;\n  const norm = (v: number) => (v - lo) / (hi - lo || 1);\n  return {\n    current,\n    previous,\n    now,\n    before,\n    // Resampled to a common length for morphing between views.\n    shape: {\n      current: resample(now.map(norm), SAMPLES),\n      previous: resample(before.map(norm), SAMPLES),\n    } as Shape,\n    // Drawn through the real days once it lands, so the dots sit on it.\n    paths: {\n      current: toPath(now.map(norm)),\n      previous: toPath(before.map(norm)),\n    } as Paths,\n    norm,\n  };\n}\n\n/** Sum of `values` over a span of days, or all of them. */\nconst total = (values: number[], span: Span | null) =>\n  sum(span ? values.slice(span.lo, span.hi + 1) : values);\n\n/** Each segment's share of the span. */\nfunction breakdown(points: MetricPoint[], span: Span | null, count: number) {\n  const parts = new Array<number>(count).fill(0);\n  const days = span ? points.slice(span.lo, span.hi + 1) : points;\n  for (const point of days) {\n    for (let i = 0; i < count; i++) {\n      parts[i] += point.parts[i] ?? 0;\n    }\n  }\n  return parts;\n}\n\nconst easeInOut = (t: number) =>\n  t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;\n\n/** Each digit rolls on its own wheel; separators sit still. */\nfunction RollingNumber({ value }: { value: string }) {\n  const chars = value.split(\"\");\n  return (\n    <span aria-hidden className=\"inline-flex tabular-nums\">\n      {chars.map((char, i) => {\n        // Keyed from the right, so units stay put when the length changes.\n        const key = chars.length - i;\n        if (!DIGIT.test(char)) {\n          return <span key={`s${key}`}>{char}</span>;\n        }\n        return (\n          <span\n            className=\"relative inline-block h-[1em] overflow-hidden leading-none\"\n            key={`d${key}`}\n          >\n            <span className=\"invisible\">0</span>\n            <span\n              className=\"absolute inset-x-0 top-0 flex flex-col transition-transform duration-[380ms] ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\"\n              style={{ transform: `translateY(-${Number(char) * 10}%)` }}\n            >\n              {DIGITS.map((d) => (\n                <span className=\"block h-[1em] leading-none\" key={d}>\n                  {d}\n                </span>\n              ))}\n            </span>\n          </span>\n        );\n      })}\n    </span>\n  );\n}\n\ntype Period = ReturnType<typeof buildPeriod>;\n\n/** A range once the drag covers two days or more, else the cursor's day. */\nfunction spanOf(\n  range: { a: number; b: number } | null,\n  cursor: number | null,\n  last: number\n): { span: Span | null; ranged: boolean } {\n  if (range && range.a !== range.b) {\n    return {\n      span: { lo: Math.min(range.a, range.b), hi: Math.max(range.a, range.b) },\n      ranged: true,\n    };\n  }\n  if (cursor !== null && cursor <= last) {\n    return { span: { lo: cursor, hi: cursor }, ranged: false };\n  }\n  return { span: null, ranged: false };\n}\n\n/** What the card reads: a range, one day, or the whole period. */\nfunction readOf(\n  info: Period,\n  span: Span | null,\n  ranged: boolean,\n  period: number,\n  events: MetricEvent[]\n) {\n  const day = span && !ranged ? info.current[span.lo] : undefined;\n  const twin = span && !ranged ? info.previous[span.lo] : undefined;\n  const value = total(info.now, span);\n  const compare = total(info.before, span);\n  const event = day ? events.find((e) => e.date === day.date) : undefined;\n  let caption = `vs previous ${period} days`;\n  if (ranged && span) {\n    const days = span.hi - span.lo + 1;\n    caption = `${formatShort(info.current[span.lo].date)} – ${formatShort(info.current[span.hi].date)} vs prior ${days} days`;\n  } else if (day) {\n    caption = `${formatDay(day.date)} vs ${formatShort(twin?.date ?? day.date)}`;\n  }\n  return {\n    day,\n    twin,\n    value,\n    delta: compare ? (value - compare) / compare : 0,\n    event,\n    caption,\n  };\n}\n\n/** Each segment's share of what the card reads. Pick one to chart it alone. */\nfunction Breakdown({\n  segments,\n  parts,\n  focus,\n  label,\n  format,\n  onChoose,\n}: {\n  segments: string[];\n  parts: number[];\n  focus: number | null;\n  label: string;\n  format: (value: number) => string;\n  onChoose: (index: number) => void;\n}) {\n  const partsTotal = sum(parts);\n  // The charted segment takes the ink, so its swatch matches the line.\n  const swatch = (i: number) => SWATCHES[focus === i ? 0 : i % SWATCHES.length];\n  return (\n    <div className=\"mt-4 border-zinc-100 border-t pt-4 dark:border-zinc-900\">\n      <div aria-hidden className=\"flex h-1.5 gap-0.5\">\n        {segments.map((name, i) => (\n          <span\n            className={cn(\n              \"min-w-0 rounded-full transition-[flex-grow,opacity] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\",\n              swatch(i),\n              focus !== null && focus !== i && \"opacity-25\"\n            )}\n            key={name}\n            style={{ flexGrow: parts[i] ?? 0 }}\n          />\n        ))}\n      </div>\n      <ul className=\"-mx-2 mt-2.5\">\n        {segments.map((name, i) => {\n          const share = partsTotal ? (parts[i] / partsTotal) * 100 : 0;\n          return (\n            <li key={name}>\n              <button\n                aria-label={\n                  focus === i ? `Show all of ${label}` : `Show only ${name}`\n                }\n                aria-pressed={focus === i}\n                className={cn(\n                  \"flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left text-sm outline-none transition-[background-color,opacity] duration-150 ease-out hover:bg-zinc-50 focus-visible:ring-2 focus-visible:ring-zinc-400 dark:hover:bg-zinc-900\",\n                  focus === i &&\n                    \"bg-zinc-100 hover:bg-zinc-100 dark:bg-zinc-900\",\n                  focus !== null && focus !== i && \"opacity-50\"\n                )}\n                onClick={() => onChoose(i)}\n                type=\"button\"\n              >\n                <span\n                  className={cn(\"size-2 shrink-0 rounded-full\", swatch(i))}\n                />\n                <span className=\"flex-1 truncate text-zinc-600 dark:text-zinc-300\">\n                  {name}\n                </span>\n                <span className=\"font-medium tabular-nums\">\n                  {format(parts[i] ?? 0)}\n                </span>\n                <span className=\"w-10 text-right text-zinc-400 tabular-nums dark:text-zinc-500\">\n                  {share.toFixed(0)}%\n                </span>\n              </button>\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n}\n\n/** Arrow keys walk the days; with Shift they stretch a range from the cursor. */\nfunction keyStep(key: string, last: number) {\n  const moves: Record<string, number> = {\n    ArrowLeft: -1,\n    ArrowRight: 1,\n    Home: -last,\n    End: last,\n  };\n  return moves[key];\n}\n\nexport default function MetricCard({\n  label = \"Revenue\",\n  segments = [\"Plans\", \"Usage\", \"Services\"],\n  data = DEFAULT_DATA,\n  events = DEFAULT_EVENTS,\n  periods = [7, 30, 90],\n  defaultPeriod = 30,\n  format = \"currency\",\n  currency = \"USD\",\n  className,\n}: MetricCardProps) {\n  const clipId = `${useId().replace(/:/g, \"\")}-lit`;\n  const [period, setPeriod] = useState(defaultPeriod);\n  const [focus, setFocus] = useState<number | null>(null);\n  // The day under the pointer or keyboard cursor.\n  const [cursor, setCursor] = useState<number | null>(null);\n  // A pinned range: where the drag began and where it is now.\n  const [range, setRange] = useState<{ a: number; b: number } | null>(null);\n  const drag = useRef<number | null>(null);\n  const info = useMemo(\n    () => buildPeriod(data, period, focus),\n    [data, period, focus]\n  );\n\n  const currentRef = useRef<SVGPathElement>(null);\n  const currentDimRef = useRef<SVGPathElement>(null);\n  const areaRef = useRef<SVGPathElement>(null);\n  const previousRef = useRef<SVGPathElement>(null);\n  const chartRef = useRef<HTMLDivElement>(null);\n  const shown = useRef<Shape>(info.shape);\n  const morph = useRef({ raf: 0 });\n\n  const money = useMemo(\n    () =>\n      new Intl.NumberFormat(\"en-US\", {\n        style: format === \"currency\" ? \"currency\" : \"decimal\",\n        currency,\n        maximumFractionDigits: 0,\n      }),\n    [format, currency]\n  );\n\n  // A new period or segment morphs the lines from where they are now.\n  useEffect(() => {\n    const from = shown.current;\n    const to = info.shape;\n    const m = morph.current;\n    cancelAnimationFrame(m.raf);\n    const paint = (paths: Paths) => {\n      currentRef.current?.setAttribute(\"d\", paths.current);\n      currentDimRef.current?.setAttribute(\"d\", paths.current);\n      areaRef.current?.setAttribute(\"d\", toArea(paths.current));\n      previousRef.current?.setAttribute(\"d\", paths.previous);\n    };\n    const land = () => {\n      shown.current = to;\n      paint(info.paths);\n    };\n    if (\n      from === to ||\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    ) {\n      land();\n      return;\n    }\n    const start = performance.now();\n    const frame = (now: number) => {\n      const t = easeInOut(Math.min(1, (now - start) / MORPH_MS));\n      const mix = (a: number[], b: number[]) =>\n        a.map((v, i) => v + (b[i] - v) * t);\n      shown.current = {\n        current: mix(from.current, to.current),\n        previous: mix(from.previous, to.previous),\n      };\n      if (t < 1) {\n        paint({\n          current: toPath(shown.current.current),\n          previous: toPath(shown.current.previous),\n        });\n        m.raf = requestAnimationFrame(frame);\n      } else {\n        land();\n      }\n    };\n    m.raf = requestAnimationFrame(frame);\n    return () => cancelAnimationFrame(m.raf);\n  }, [info.shape, info.paths]);\n\n  const last = info.current.length - 1;\n\n  const pinned = useMemo(\n    () =>\n      events\n        .map((e) => ({\n          ...e,\n          at: info.current.findIndex((p) => p.date === e.date),\n        }))\n        .filter((e) => e.at >= 0),\n    [events, info.current]\n  );\n\n  const indexAt = useCallback(\n    (clientX: number) => {\n      const rect = chartRef.current?.getBoundingClientRect();\n      if (!rect) {\n        return 0;\n      }\n      const t = clamp((clientX - rect.left) / rect.width, 0, 1);\n      // Pinned days pull the cursor in, so their notes are easy to land on.\n      const near = pinned.find(\n        (e) => Math.abs((e.at / (last || 1) - t) * rect.width) < SNAP_PX\n      );\n      return near ? near.at : Math.round(t * last);\n    },\n    [last, pinned]\n  );\n\n  const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {\n    if (event.pointerType === \"mouse\" && event.button !== 0) {\n      return;\n    }\n    event.currentTarget.setPointerCapture(event.pointerId);\n    const at = indexAt(event.clientX);\n    drag.current = at;\n    setRange(null);\n    setCursor(at);\n  };\n\n  const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {\n    const at = indexAt(event.clientX);\n    if (drag.current !== null) {\n      // Dragging past the first day turns the read into a range.\n      const anchor = drag.current;\n      setRange((r) => (r || at !== anchor ? { a: anchor, b: at } : null));\n      setCursor(at);\n    } else if (event.pointerType === \"mouse\" && !range) {\n      // Mouse reads on hover; touch only while pressed.\n      setCursor(at);\n    }\n  };\n\n  const release = (event: PointerEvent<HTMLDivElement>) => {\n    drag.current = null;\n    if (event.pointerType !== \"mouse\") {\n      setCursor(null);\n    }\n  };\n\n  const clearRead = () => {\n    setRange(null);\n    setCursor(null);\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (event.key === \"Escape\") {\n      clearRead();\n      return;\n    }\n    const move = keyStep(event.key, last);\n    if (move === undefined) {\n      return;\n    }\n    event.preventDefault();\n    const from = range ? range.b : (cursor ?? last);\n    const to = clamp(from + move, 0, last);\n    if (event.shiftKey) {\n      setRange({ a: range ? range.a : from, b: to });\n    } else {\n      setRange(null);\n    }\n    setCursor(to);\n  };\n\n  const choosePeriod = (days: number) => {\n    clearRead();\n    setPeriod(days);\n  };\n\n  const chooseSegment = (index: number) => {\n    setFocus((f) => (f === index ? null : index));\n  };\n\n  const { span, ranged } = spanOf(range, cursor, last);\n  const { day, twin, value, delta, event, caption } = readOf(\n    info,\n    span,\n    ranged,\n    period,\n    events\n  );\n  const up = delta >= 0;\n  const parts = breakdown(info.current, span, segments.length);\n  const formatted = money.format(value);\n  let valueText = `${formatted} in total`;\n  if (ranged) {\n    valueText = `${caption}: ${formatted}`;\n  } else if (day) {\n    valueText = `${formatDay(day.date)}: ${formatted}${event ? `, ${event.label}` : \"\"}`;\n  }\n\n  const xOf = (i: number) => (last ? (i / last) * W : W);\n  const pct = (i: number) => (xOf(i) / W) * 100;\n  // The lit part of the line: up to the read day, across the range, or all.\n  const litFrom = ranged && span ? xOf(span.lo) : 0;\n  const litTo = span ? xOf(span.hi) : W;\n  const endY = info.now.length ? yOf(info.norm(info.now[last])) : 0;\n  const firstDay = info.current[0];\n  const lastDay = info.current.at(-1);\n  const periodIndex = Math.max(0, periods.indexOf(period));\n\n  return (\n    <div\n      className={cn(\n        \"w-full max-w-md select-none rounded-3xl border border-zinc-200 bg-white p-5 text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100\",\n        className\n      )}\n    >\n      <div className=\"flex items-center justify-between gap-4\">\n        <p className=\"truncate text-sm text-zinc-500 dark:text-zinc-400\">\n          {label}\n          {focus !== null && (\n            <span className=\"text-zinc-900 dark:text-zinc-100\">\n              {\" \"}\n              · {segments[focus]}\n            </span>\n          )}\n        </p>\n        <div className=\"relative flex shrink-0 rounded-full bg-zinc-100 p-0.5 text-xs dark:bg-zinc-900\">\n          {/* One pill slides between the periods. */}\n          <span\n            aria-hidden\n            className=\"absolute top-0.5 bottom-0.5 left-0.5 w-10 rounded-full bg-white shadow-sm transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none dark:bg-zinc-700\"\n            style={{ transform: `translateX(${periodIndex * 100}%)` }}\n          />\n          {periods.map((days) => (\n            <button\n              aria-pressed={days === period}\n              className={cn(\n                \"relative w-10 rounded-full py-1 font-medium tabular-nums outline-none transition-colors duration-200 ease-out focus-visible:ring-2 focus-visible:ring-zinc-400\",\n                days === period\n                  ? \"text-zinc-900 dark:text-zinc-50\"\n                  : \"text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100\"\n              )}\n              key={days}\n              onClick={() => choosePeriod(days)}\n              type=\"button\"\n            >\n              {days}D\n            </button>\n          ))}\n        </div>\n      </div>\n\n      <p className=\"mt-3 font-semibold text-4xl tracking-tight\">\n        <span className=\"sr-only\">{formatted}</span>\n        <RollingNumber value={formatted} />\n      </p>\n      <div className=\"mt-1.5 flex min-h-5 items-center gap-2 text-sm tabular-nums\">\n        <span\n          className={cn(\n            \"shrink-0 font-medium\",\n            up\n              ? \"text-emerald-600 dark:text-emerald-400\"\n              : \"text-rose-600 dark:text-rose-400\"\n          )}\n        >\n          {up ? \"▲\" : \"▼\"} {Math.abs(delta * 100).toFixed(1)}%\n        </span>\n        <span\n          aria-live=\"polite\"\n          className=\"truncate text-zinc-500 dark:text-zinc-400\"\n        >\n          {caption}\n          {event && (\n            <span className=\"text-orange-600 dark:text-orange-400\">\n              {\" \"}\n              · {event.label}\n            </span>\n          )}\n        </span>\n        {ranged && (\n          <button\n            className=\"ml-auto shrink-0 rounded-full px-2 py-0.5 text-xs text-zinc-500 outline-none transition-colors duration-150 ease-out hover:bg-zinc-100 hover:text-zinc-900 focus-visible:ring-2 focus-visible:ring-zinc-400 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-100\"\n            onClick={clearRead}\n            type=\"button\"\n          >\n            Clear\n          </button>\n        )}\n      </div>\n\n      <div\n        aria-label={`${label} over the last ${period} days. Arrow keys read each day; hold Shift to select a range.`}\n        aria-valuemax={info.current.length}\n        aria-valuemin={1}\n        aria-valuenow={span ? span.hi + 1 : info.current.length}\n        aria-valuetext={valueText}\n        className=\"relative mt-5 h-32 cursor-crosshair touch-pan-y rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-4 dark:focus-visible:ring-offset-zinc-950\"\n        onBlur={() => setCursor(null)}\n        onKeyDown={handleKeyDown}\n        onLostPointerCapture={release}\n        onPointerCancel={release}\n        onPointerDown={handlePointerDown}\n        onPointerLeave={() => {\n          if (drag.current === null && !range) {\n            setCursor(null);\n          }\n        }}\n        onPointerMove={handlePointerMove}\n        onPointerUp={release}\n        ref={chartRef}\n        role=\"slider\"\n        tabIndex={0}\n      >\n        <svg\n          aria-hidden=\"true\"\n          className=\"absolute inset-0 h-full w-full overflow-visible\"\n          preserveAspectRatio=\"none\"\n          viewBox={`0 0 ${W} ${H}`}\n        >\n          <defs>\n            <clipPath id={clipId}>\n              <rect\n                height={H + 20}\n                width={Math.max(0, litTo - litFrom)}\n                x={litFrom}\n                y={-10}\n              />\n            </clipPath>\n          </defs>\n          <line\n            className=\"stroke-zinc-200 dark:stroke-zinc-800\"\n            vectorEffect=\"non-scaling-stroke\"\n            x1={0}\n            x2={W}\n            y1={H - 0.5}\n            y2={H - 0.5}\n          />\n          <path\n            className=\"fill-none stroke-zinc-300 dark:stroke-zinc-700\"\n            d={info.paths.previous}\n            ref={previousRef}\n            strokeDasharray=\"3 4\"\n            strokeWidth={1.25}\n            vectorEffect=\"non-scaling-stroke\"\n          />\n          {/* The area only fills the part being read. */}\n          <path\n            className=\"fill-zinc-900/[0.05] dark:fill-zinc-100/[0.06]\"\n            clipPath={`url(#${clipId})`}\n            d={toArea(info.paths.current)}\n            ref={areaRef}\n          />\n          <path\n            className=\"fill-none stroke-zinc-900/25 dark:stroke-zinc-100/25\"\n            d={info.paths.current}\n            ref={currentDimRef}\n            strokeLinecap=\"round\"\n            strokeWidth={2}\n            vectorEffect=\"non-scaling-stroke\"\n          />\n          <path\n            className=\"fill-none stroke-zinc-900 dark:stroke-zinc-100\"\n            clipPath={`url(#${clipId})`}\n            d={info.paths.current}\n            ref={currentRef}\n            strokeLinecap=\"round\"\n            strokeWidth={2}\n            vectorEffect=\"non-scaling-stroke\"\n          />\n        </svg>\n\n        {pinned.map((e) => (\n          <div\n            aria-hidden\n            className=\"pointer-events-none absolute top-0 bottom-0 w-px -translate-x-1/2\"\n            key={e.date}\n            style={{ left: `${pct(e.at)}%` }}\n          >\n            <div className=\"absolute inset-y-0 left-0 border-orange-500/30 border-l border-dashed dark:border-orange-400/30\" />\n            <div className=\"absolute bottom-0 left-1/2 size-1.5 -translate-x-1/2 translate-y-1/2 rounded-full bg-orange-500 ring-2 ring-white dark:bg-orange-400 dark:ring-zinc-950\" />\n          </div>\n        ))}\n\n        {ranged &&\n          span &&\n          [span.lo, span.hi].map((i) => (\n            <div\n              aria-hidden\n              className=\"pointer-events-none absolute top-0 bottom-0 w-px bg-zinc-300 dark:bg-zinc-700\"\n              key={i}\n              style={{ left: `${pct(i)}%` }}\n            />\n          ))}\n\n        {day && span && (\n          <>\n            <div\n              aria-hidden\n              className=\"pointer-events-none absolute top-0 bottom-0 w-px bg-zinc-200 dark:bg-zinc-800\"\n              style={{ left: `${pct(span.lo)}%` }}\n            />\n            {twin && (\n              <div\n                aria-hidden\n                className=\"pointer-events-none absolute size-2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-zinc-300 bg-white dark:border-zinc-600 dark:bg-zinc-950\"\n                style={{\n                  left: `${pct(span.lo)}%`,\n                  top: `${(yOf(info.norm(info.before[span.lo])) / H) * 100}%`,\n                }}\n              />\n            )}\n            <div\n              aria-hidden\n              className=\"pointer-events-none absolute size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-900 ring-4 ring-white dark:bg-zinc-100 dark:ring-zinc-950\"\n              style={{\n                left: `${pct(span.lo)}%`,\n                top: `${(yOf(info.norm(info.now[span.lo])) / H) * 100}%`,\n              }}\n            />\n          </>\n        )}\n\n        {/* Today, at rest. It rides the morph to the new end. */}\n        <div\n          aria-hidden\n          className={cn(\n            \"pointer-events-none absolute right-0 size-2 translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-900 ring-4 ring-white transition-[top,opacity] duration-[420ms] ease-[cubic-bezier(0.65,0,0.35,1)] motion-reduce:transition-none dark:bg-zinc-100 dark:ring-zinc-950\",\n            span ? \"opacity-0\" : \"opacity-100\"\n          )}\n          style={{ top: `${(endY / H) * 100}%` }}\n        />\n      </div>\n\n      <div className=\"mt-2 flex justify-between text-xs text-zinc-400 tabular-nums dark:text-zinc-500\">\n        <span>{firstDay ? formatShort(firstDay.date) : \"\"}</span>\n        <span className=\"flex items-center gap-1.5\">\n          <span className=\"h-px w-3 border-zinc-300 border-t border-dashed dark:border-zinc-700\" />\n          Previous period\n        </span>\n        <span>{lastDay ? formatShort(lastDay.date) : \"\"}</span>\n      </div>\n\n      <Breakdown\n        focus={focus}\n        format={(v) => money.format(v)}\n        label={label}\n        onChoose={chooseSegment}\n        parts={parts}\n        segments={segments}\n      />\n    </div>\n  );\n}\n",
      "path": "/components/kokonutui/metric-card.tsx",
      "target": "components/kokonutui/metric-card.tsx"
    }
  ]
}