{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bar-chart",
  "type": "registry:ui",
  "title": "Bar Chart",
  "description": "Columns or ranked rows that grow in, morph on change and glide a tooltip.",
  "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"
  ],
  "files": [
    {
      "path": "components/ui/bar-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 { ease, spring, stagger } 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\n/** Tracks an element's content width. 0 until the browser has measured it. */\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 tick values (0, 2.5k, 5k…) that cover [min, max], always including zero. */\nexport function niceTicks(min: number, max: number, count = 4) {\n  const lo0 = Math.min(0, min);\n  let hi0 = Math.max(0, max);\n  if (hi0 === lo0) hi0 = lo0 + 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 ticks: number[] = [];\n  for (let v = lo; v <= hi + step / 2; v += step) ticks.push(Math.round(v / step) * step);\n  return ticks;\n}\n\n// A bar from its baseline edge to its data end, rounded only at the data end.\n// The command structure never changes, so Motion can morph one bar into another.\nfunction barPath(vertical: boolean, band0: number, band1: number, base: number, end: number, radius: number) {\n  const size = Math.abs(end - base);\n  const r = Math.max(0, Math.min(radius, size, (band1 - band0) / 2));\n  const dir = end < base ? -1 : 1;\n  const e = end;\n  const er = end - dir * r;\n  const f = (n: number) => n.toFixed(2);\n  if (vertical)\n    return `M${f(band0)} ${f(base)}L${f(band0)} ${f(er)}Q${f(band0)} ${f(e)} ${f(band0 + r)} ${f(e)}L${f(band1 - r)} ${f(e)}Q${f(band1)} ${f(e)} ${f(band1)} ${f(er)}L${f(band1)} ${f(base)}Z`;\n  return `M${f(base)} ${f(band0)}L${f(er)} ${f(band0)}Q${f(e)} ${f(band0)} ${f(e)} ${f(band0 + r)}L${f(e)} ${f(band1 - r)}Q${f(e)} ${f(band1)} ${f(er)} ${f(band1)}L${f(base)} ${f(band1)}Z`;\n}\n\n// Series share one ink, stepped by opacity. Override per series with className (e.g. \"text-info\").\nconst inks = [0.9, 0.5, 0.28, 0.14];\n\nexport type BarSeries = {\n  /** The field in each datum that holds this series' value. */\n  key: string;\n  label: string;\n  /** Text-color class for this series' ink, e.g. \"text-info\". Defaults to the foreground at a stepped opacity. */\n  className?: string;\n};\n\nexport type BarChartProps<T extends Record<string, unknown>> = Omit<React.ComponentProps<\"div\">, \"children\" | \"onSelect\"> & {\n  data: T[];\n  /** The field that names each bar or group: a month, a referrer, a region. */\n  categoryKey: keyof T & string;\n  series: BarSeries[];\n  /** vertical draws columns up from the x axis; horizontal draws ranked rows. */\n  layout?: \"vertical\" | \"horizontal\";\n  /** Stack series on top of each other instead of side by side. Positive values only. */\n  stacked?: boolean;\n  /** Names the chart for screen readers. */\n  label: string;\n  /** Plot height in pixels for vertical charts. Horizontal charts grow with their rows. */\n  height?: number;\n  /** Row height in pixels for horizontal charts. */\n  rowHeight?: number;\n  formatValue?: (value: number) => string;\n  formatCategory?: (category: string) => string;\n  locale?: string;\n  /** Print each bar's value at its end. Defaults on for horizontal, off for vertical. */\n  valueLabels?: boolean;\n  /** A target or average line across the plot. */\n  reference?: { value: number; label: string };\n  /** Bars become buttons: Enter, Space or a click calls this with the datum. */\n  onBarClick?: (datum: T, index: number) => void;\n  /** Loading: a skeleton with no data, or the last data dimmed while it refreshes. */\n  loading?: boolean;\n  /** Shown in the plot when data is empty. */\n  emptyLabel?: string;\n  /** Grow the bars from the baseline the first time the chart scrolls into view. */\n  animated?: boolean;\n  /** A key above the plot. Defaults on when there is more than one series. */\n  legend?: boolean;\n};\n\nexport function BarChart<T extends Record<string, unknown>>({\n  data,\n  categoryKey,\n  series,\n  layout = \"vertical\",\n  stacked = false,\n  label,\n  height = 220,\n  rowHeight = 28,\n  formatValue,\n  formatCategory = (c) => c,\n  locale: localeProp,\n  valueLabels,\n  reference,\n  onBarClick,\n  loading = false,\n  emptyLabel = \"No data for this period\",\n  animated = true,\n  legend,\n  className,\n  ...rest\n}: BarChartProps<T>) {\n  const vertical = layout === \"vertical\";\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 [hover, setHover] = useState<number | null>(null);\n  const [focus, setFocus] = useState<number | null>(null);\n  const [pinned, setPinned] = useState(false);\n  const tooltipId = useId();\n  const showValues = valueLabels ?? !vertical;\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 num = (d: T, key: string) => {\n    const v = Number(d[key]);\n    return Number.isFinite(v) ? v : 0;\n  };\n  const totals = data.map((d) => series.reduce((s, x) => s + Math.max(0, num(d, x.key)), 0));\n  const values = data.flatMap((d) => series.map((s) => num(d, s.key)));\n  const lo = stacked ? 0 : Math.min(0, ...values);\n  const hi = stacked ? Math.max(0, ...totals) : Math.max(0, ...values, reference?.value ?? 0);\n  const ticks = niceTicks(lo, Math.max(hi, reference?.value ?? 0), vertical ? 4 : 3);\n  // Rows that print their own values don't need an axis: direct labels beat gridlines.\n  const axis = vertical || !showValues;\n  const t0 = ticks[0];\n  const t1 = ticks[ticks.length - 1];\n\n  // Layout, in pixels. Gutters are estimated from the longest label rather than measured.\n  const tickWidth = Math.max(...ticks.map((t) => fmt(t).length)) * 6.4 + 10;\n  const catNeed = vertical ? 0 : Math.max(...data.map((d) => formatCategory(String(d[categoryKey])).length), 4) * 6.6 + 14;\n  // On a narrow chart, row names move above their bars instead of truncating.\n  const labelsAbove = !vertical && width > 0 && catNeed > width * 0.34;\n  const catWidth = vertical || labelsAbove ? 0 : catNeed;\n  const valueWidth = !vertical && showValues ? Math.max(...values.map((v) => fmt(v).length), stacked ? Math.max(...totals.map((v) => fmt(v).length)) : 0, 3) * 6.6 + 10 : 0;\n  const m = vertical\n    ? { top: showValues ? 20 : 10, right: reference ? Math.max(reference.label.length, fmt(reference.value).length) * 6.2 + 14 : 4, bottom: 26, left: tickWidth }\n    : { top: reference ? 20 : 4, right: valueWidth + 4, bottom: axis ? 22 : 4, left: catWidth };\n  const labelBand = labelsAbove ? 16 : 0;\n  const plotH = vertical ? height : (data.length || 5) * (labelsAbove ? rowHeight - 6 + labelBand : rowHeight);\n  const totalH = plotH + m.top + m.bottom;\n  const plotW = Math.max(0, width - m.left - m.right);\n  const extent = vertical ? plotH : plotW;\n  const scale = (v: number) => {\n    const p = (v - t0) / (t1 - t0 || 1);\n    return vertical ? m.top + plotH - p * plotH : m.left + p * plotW;\n  };\n  const baseline = scale(0);\n  const bandSize = (vertical ? plotW : plotH) / Math.max(1, data.length);\n  const bandStart = (i: number) => (vertical ? m.left : m.top) + i * bandSize;\n  const barCenter = (i: number) => bandStart(i) + labelBand + (bandSize - labelBand) / 2;\n  // Bars never fill the band: at most 24px (per series when grouped), the rest is air.\n  const groupSlots = stacked ? 1 : series.length;\n  const barThick = Math.max(2, Math.min(vertical ? 24 : 16, ((bandSize - labelBand) * (vertical ? 0.62 : 0.6)) / groupSlots));\n  const groupThick = barThick * groupSlots + (groupSlots - 1) * 2;\n  const gap = 2;\n\n  // d0 is the same bar collapsed onto the baseline: where the first grow starts.\n  type Seg = { key: string; si: number; d: string; d0: string; value: number; end: number };\n  const bars: Seg[][] = data.map((d, i) => {\n    const c = barCenter(i);\n    const g0 = c - groupThick / 2;\n    if (stacked) {\n      let acc = 0;\n      const visible = series.map((s) => Math.max(0, num(d, s.key)));\n      const topIndex = visible.reduce((top, v, si) => (v > 0 ? si : top), -1);\n      return series.map((s, si) => {\n        const v = visible[si];\n        const from = scale(acc);\n        acc += v;\n        const to = scale(acc);\n        // A 2px surface gap between segments, taken from the segment above.\n        const start = si > 0 && v > 0 && acc - v > 0 ? from + (vertical ? -gap : gap) : from;\n        const end = vertical ? Math.min(start, to) : Math.max(start, to);\n        const r = si === topIndex ? 4 : 0;\n        return { key: s.key, si, value: v, end, d: barPath(vertical, g0, g0 + barThick, start, end, r), d0: barPath(vertical, g0, g0 + barThick, baseline, baseline, r) };\n      });\n    }\n    return series.map((s, si) => {\n      const v = num(d, s.key);\n      const b0 = g0 + si * (barThick + 2);\n      const end = scale(v);\n      return { key: s.key, si, value: v, end, d: barPath(vertical, b0, b0 + barThick, baseline, end, 4), d0: barPath(vertical, b0, b0 + barThick, baseline, baseline, 4) };\n    });\n  });\n\n  const empty = !loading && data.length === 0;\n  const skeleton = loading && data.length === 0;\n  const grown = !animated || inView;\n  // The first grow is staggered and slower; every later change morphs in place.\n  const [settled, setSettled] = useState(!animated);\n  useEffect(() => {\n    if (!grown || settled) return;\n    const t = window.setTimeout(() => setSettled(true), 1000);\n    return () => window.clearTimeout(t);\n  }, [grown, settled]);\n  const active = hover ?? focus;\n  const clickable = !!onBarClick;\n\n  const indexAt = (e: React.PointerEvent) => {\n    const r = e.currentTarget.getBoundingClientRect();\n    const pos = vertical ? e.clientX - r.left - m.left : e.clientY - r.top - m.top;\n    const i = Math.floor(pos / bandSize);\n    return i >= 0 && i < data.length ? i : null;\n  };\n\n  // A tap pins the tooltip on touch; the next tap outside the chart lets it go.\n  useEffect(() => {\n    if (!pinned) return;\n    const off = (e: PointerEvent) => {\n      if (!root.current?.contains(e.target as Node)) {\n        setPinned(false);\n        setHover(null);\n      }\n    };\n    document.addEventListener(\"pointerdown\", off);\n    return () => document.removeEventListener(\"pointerdown\", off);\n  }, [pinned]);\n\n  // Tooltip anchor. Columns: beside the bar, on the side with more room, so it never\n  // covers the bar it describes. Rows: above the row, at the bar's end.\n  const tx = useMotionValue(0);\n  const ty = useMotionValue(0);\n  const ts = useMotionValue(0);\n  const tipLeft = useTransform(tx, (v) => `${v}px`);\n  const tipTop = useTransform(ty, (v) => `${v}px`);\n  const tipShift = useTransform(ts, (v) => `${-v * 100}%`);\n  const anchor = (i: number) => {\n    const c = barCenter(i);\n    if (vertical) {\n      const right = c < width / 2;\n      return { x: right ? c + groupThick / 2 + 10 : c - groupThick / 2 - 10, y: m.top, s: right ? 0 : 1 };\n    }\n    const far = Math.max(...bars[i].map((b) => b.end), baseline);\n    const x = Math.min(far, width);\n    return { x, y: c - groupThick / 2 - 6, s: Math.min(1, Math.max(0, x / (width || 1))) };\n  };\n  // Rows that already print their value only need a tooltip when there's more than one series.\n  const tipWanted = vertical || series.length > 1 || !showValues;\n  const tipVisible = tipWanted && active != null && !skeleton && !empty && active < data.length;\n  const target = tipVisible && active != null ? anchor(active) : null;\n  const ax = target?.x ?? null;\n  const ay = target?.y ?? null;\n  const as = target?.s ?? null;\n  const wasVisible = useRef(false);\n  const keyboard = focus != null && hover == null;\n  useEffect(() => {\n    if (ax == null || ay == null || as == null) {\n      wasVisible.current = false;\n      return;\n    }\n    // The first appearance lands in place; after that it glides between bars. Keys jump.\n    if (!wasVisible.current || reduce || keyboard) {\n      tx.jump(ax);\n      ty.jump(ay);\n      ts.jump(as);\n    } else {\n      animate(tx, ax, spring.follow);\n      animate(ty, ay, spring.follow);\n      animate(ts, as, spring.follow);\n    }\n    wasVisible.current = true;\n  }, [ax, ay, as, reduce, keyboard, tx, ty, ts]);\n\n  const inkOf = (si: number) => (series[si]?.className ? 1 : inks[Math.min(si, inks.length - 1)]);\n  const catLabel = (i: number) => formatCategory(String(data[i][categoryKey]));\n  const describe = (i: number) => {\n    const parts = series.map((s, si) => `${s.label} ${fmt(bars[i][si].value)}`);\n    if (stacked && series.length > 1) parts.push(`total ${fmt(totals[i])}`);\n    return `${catLabel(i)}: ${parts.join(\", \")}`;\n  };\n\n  // Category labels thin out when they'd collide: every 2nd, 3rd… label, always keeping the last.\n  const labelEvery = vertical ? Math.max(1, Math.ceil((Math.max(...data.map((_, i) => catLabel(i).length), 1) * 6.4 + 12) / Math.max(bandSize, 1))) : 1;\n\n  const onKey = (e: React.KeyboardEvent, i: number) => {\n    const next = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[e.key];\n    let to: number | undefined;\n    if (next) to = Math.min(data.length - 1, Math.max(0, i + next));\n    else if (e.key === \"Home\") to = 0;\n    else if (e.key === \"End\") to = data.length - 1;\n    else if ((e.key === \"Enter\" || e.key === \" \") && onBarClick) {\n      e.preventDefault();\n      onBarClick(data[i], i);\n      return;\n    } else if (e.key === \"Escape\") {\n      setFocus(null);\n      return;\n    }\n    if (to == null) return;\n    e.preventDefault();\n    const target = e.currentTarget.parentElement?.querySelector<SVGElement>(`[data-index=\"${to}\"]`);\n    target?.focus();\n  };\n\n  // One tab stop for the whole chart: the focused bar, or the first.\n  const [rover, setRover] = useState(0);\n  const tabbable = Math.min(rover, Math.max(0, data.length - 1));\n\n  return (\n    <div\n      ref={root}\n      data-slot=\"bar-chart\"\n      data-layout={layout}\n      data-state={skeleton ? \"loading\" : empty ? \"empty\" : loading ? \"refreshing\" : \"ready\"}\n      aria-busy={loading || undefined}\n      className={cn(\"flex w-full min-w-0 select-none flex-col gap-3 text-fg\", className)}\n      {...rest}\n    >\n      {(legend ?? series.length > 1) && (\n        <ul aria-label=\"Legend\" className=\"flex flex-wrap items-center gap-x-3.5 gap-y-1 text-[11.5px] text-fg-2\">\n          {series.map((s, si) => (\n            <li key={s.key} className=\"flex items-center gap-1.5\">\n              <span className={cn(\"size-2 rounded-[2px] bg-current\", s.className ?? \"text-fg\")} style={{ opacity: inkOf(si) }} />\n              {s.label}\n            </li>\n          ))}\n        </ul>\n      )}\n      <div ref={measure} className=\"relative w-full\" style={{ height: totalH }}>\n      {width > 0 && (\n        <>\n          <svg\n            width={width}\n            height={totalH}\n            role=\"group\"\n            aria-label={label}\n            aria-roledescription=\"bar chart\"\n            className={cn(\"block overflow-visible transition-opacity duration-200\", loading && data.length > 0 && \"opacity-50\")}\n            onPointerMove={(e) => {\n              if (skeleton || empty) return;\n              if (e.pointerType === \"mouse\" || !pinned) setHover(indexAt(e));\n            }}\n            onPointerDown={(e) => {\n              if (e.pointerType === \"mouse\" || skeleton || empty) return;\n              setHover(indexAt(e));\n              setPinned(true);\n            }}\n            onPointerLeave={(e) => {\n              if (e.pointerType === \"mouse\") setHover(null);\n            }}\n            onClick={(e) => {\n              if (!onBarClick) return;\n              const r = e.currentTarget.getBoundingClientRect();\n              const pos = vertical ? e.clientX - r.left - m.left : e.clientY - r.top - m.top;\n              const i = Math.floor(pos / bandSize);\n              if (i >= 0 && i < data.length) onBarClick(data[i], i);\n            }}\n          >\n            {/* Grid: hairlines at each tick, the zero line one step stronger. */}\n            {/* Ticks slide with the bars when the scale changes, so the grid never disagrees with the data mid-flight. */}\n            <g aria-hidden>\n              <AnimatePresence initial={false}>\n                {ticks.filter((t) => axis || (t === 0 && !labelsAbove && data.length > 0)).map((t) => {\n                  const p = Math.round(scale(t)) + 0.5;\n                  const at = vertical ? { y1: p, y2: p } : { x1: p, x2: p };\n                  return (\n                    <motion.line\n                      key={t}\n                      {...(vertical ? { x1: m.left, x2: width - m.right } : { y1: m.top, y2: m.top + plotH })}\n                      className={t === 0 ? \"stroke-line-2\" : \"stroke-line\"}\n                      strokeWidth={1}\n                      initial={{ opacity: 0, ...at }}\n                      animate={{ opacity: 1, ...at }}\n                      exit={{ opacity: 0, transition: { duration: 0.12 } }}\n                      transition={{ duration: reduce ? 0 : 0.45, ease: ease.inOut }}\n                    />\n                  );\n                })}\n              </AnimatePresence>\n            </g>\n\n            {skeleton && (\n              <g aria-hidden className=\"motion-safe:animate-pulse-soft\">\n                {Array.from({ length: vertical ? 8 : 5 }, (_, i) => {\n                  const n = vertical ? 8 : 5;\n                  const size = (vertical ? plotW : plotH) / n;\n                  const b0 = (vertical ? m.left : m.top) + i * size + size / 2 - Math.min(vertical ? 24 : 16, size * 0.6) / 2;\n                  const b1 = b0 + Math.min(vertical ? 24 : 16, size * 0.6);\n                  const frac = [0.45, 0.62, 0.38, 0.7, 0.55, 0.8, 0.5, 0.66][i];\n                  const end = vertical ? baseline - frac * extent * 0.9 : m.left + frac * plotW * 0.9;\n                  return <path key={i} d={barPath(vertical, b0, b1, baseline, end, 4)} className=\"fill-fg/[0.06]\" />;\n                })}\n              </g>\n            )}\n\n            {bars.map((segs, i) => {\n              const dim = active != null && active !== i;\n              return (\n                <g\n                  key={String(data[i][categoryKey])}\n                  data-index={i}\n                  tabIndex={i === tabbable ? 0 : -1}\n                  role={clickable ? \"button\" : \"img\"}\n                  aria-label={describe(i)}\n                  aria-describedby={active === i ? tooltipId : undefined}\n                  className={cn(\"outline-none transition-opacity duration-150\", clickable && \"cursor-pointer\")}\n                  style={{ opacity: dim ? 0.35 : 1 }}\n                  onFocus={(e) => {\n                    setRover(i);\n                    if ((e.currentTarget as Element).matches(\":focus-visible\")) setFocus(i);\n                  }}\n                  onBlur={() => setFocus(null)}\n                  onKeyDown={(e) => onKey(e, i)}\n                >\n                  {/* The hit area is the whole band, not just the painted bar. */}\n                  <rect\n                    x={vertical ? bandStart(i) : m.left}\n                    y={vertical ? m.top : bandStart(i)}\n                    width={vertical ? bandSize : plotW}\n                    height={vertical ? plotH : bandSize}\n                    fill=\"transparent\"\n                  />\n                  {focus === i && (\n                    <rect\n                      aria-hidden\n                      x={vertical ? bandStart(i) + 2 : m.left - 4}\n                      y={vertical ? m.top - 4 : bandStart(i) + 1}\n                      width={vertical ? bandSize - 4 : plotW + 8}\n                      height={vertical ? plotH + 8 : bandSize - 2}\n                      rx={6}\n                      fill=\"none\"\n                      className=\"stroke-fg-3\"\n                      strokeWidth={1}\n                    />\n                  )}\n                  {segs.map((s) => (\n                    <motion.path\n                      key={s.key}\n                      className={series[s.si].className}\n                      fill=\"currentColor\"\n                      fillOpacity={inkOf(s.si)}\n                      initial={false}\n                      animate={{ d: grown ? s.d : s.d0 }}\n                      transition={\n                        reduce\n                          ? { duration: 0 }\n                          : !settled\n                            ? { duration: 0.6, ease: ease.out, delay: Math.min(i, 8) * stagger.items * 2 }\n                            : { duration: 0.45, ease: ease.inOut }\n                      }\n                    />\n                  ))}\n                </g>\n              );\n            })}\n\n            {reference && data.length > 0 && (\n              <g aria-hidden>\n                <motion.line\n                  {...(vertical ? { x1: m.left, x2: width - m.right } : { y1: m.top, y2: m.top + plotH })}\n                  className=\"stroke-fg-3\"\n                  strokeWidth={1}\n                  initial={false}\n                  animate={vertical ? { y1: Math.round(scale(reference.value)) + 0.5, y2: Math.round(scale(reference.value)) + 0.5 } : { x1: Math.round(scale(reference.value)) + 0.5, x2: Math.round(scale(reference.value)) + 0.5 }}\n                  transition={{ duration: reduce ? 0 : 0.45, ease: ease.inOut }}\n                />\n              </g>\n            )}\n          </svg>\n\n          {/* Text is HTML so it inherits the type, truncates, and never scales with the SVG. */}\n          <div aria-hidden className=\"pointer-events-none absolute inset-0 text-2xs text-fg-3 tabular\">\n            <AnimatePresence initial={false}>\n              {ticks.filter(() => axis && data.length > 0).map((t) => (\n                <motion.span\n                  key={t}\n                  className={cn(\"absolute whitespace-nowrap\", vertical ? \"left-0 -translate-y-1/2 pr-2 text-right\" : \"-translate-x-1/2\")}\n                  style={vertical ? { width: m.left } : { top: m.top + plotH + 6 }}\n                  initial={{ opacity: 0, ...(vertical ? { top: scale(t) } : { left: scale(t) }) }}\n                  animate={{ opacity: 1, ...(vertical ? { top: scale(t) } : { left: scale(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            {!skeleton &&\n              data.map((_, i) => {\n                const show = !vertical || i % labelEvery === 0 || (i === data.length - 1 && (data.length - 1) % labelEvery >= labelEvery / 2);\n                if (!show) return null;\n                const on = active === i;\n                return vertical ? (\n                  <span\n                    key={i}\n                    className={cn(\"absolute -translate-x-1/2 truncate text-center text-[11px] transition-colors duration-150\", on ? \"text-fg\" : \"text-fg-3\")}\n                    style={{ left: bandStart(i) + bandSize / 2, top: m.top + plotH + 8, maxWidth: bandSize * labelEvery }}\n                  >\n                    {catLabel(i)}\n                  </span>\n                ) : (\n                  <span\n                    key={i}\n                    className={cn(\"absolute left-0 -translate-y-1/2 truncate pr-3 text-[12px] transition-colors duration-150\", on ? \"text-fg\" : \"text-fg-2\")}\n                    style={labelsAbove ? { top: bandStart(i) + labelBand / 2 + 2, width: \"100%\" } : { top: barCenter(i), width: m.left }}\n                  >\n                    {catLabel(i)}\n                  </span>\n                );\n              })}\n            {showValues &&\n              !skeleton &&\n              bars.map((segs, i) => {\n                const v = stacked ? totals[i] : null;\n                if (vertical) {\n                  const top = Math.min(...segs.map((s) => s.end), baseline);\n                  return (\n                    <motion.span\n                      key={i}\n                      className=\"absolute -translate-x-1/2 -translate-y-full pb-1 text-[11px] text-fg-2\"\n                      style={{ left: bandStart(i) + bandSize / 2, top }}\n                      initial={false}\n                      animate={{ opacity: grown && (active == null || active === i) ? 1 : grown ? 0.35 : 0 }}\n                      transition={{ duration: 0.2, delay: !settled && !reduce ? 0.5 : 0 }}\n                    >\n                      {fmt(v ?? segs[0].value)}\n                    </motion.span>\n                  );\n                }\n                return segs.map((s, si) =>\n                  stacked && si > 0 ? null : (\n                    <motion.span\n                      key={`${i}-${s.key}`}\n                      className=\"absolute -translate-y-1/2 whitespace-nowrap pl-2 text-[12px] text-fg-2\"\n                      style={{\n                        top: stacked ? barCenter(i) : barCenter(i) - groupThick / 2 + si * (barThick + 2) + barThick / 2,\n                        left: stacked ? Math.max(...segs.map((x) => x.end)) : s.end,\n                      }}\n                      initial={false}\n                      animate={{ opacity: grown ? (active == null || active === i ? 1 : 0.35) : 0 }}\n                      transition={{ duration: 0.2, delay: !settled && !reduce ? 0.45 : 0 }}\n                    >\n                      {fmt(v ?? s.value)}\n                    </motion.span>\n                  ),\n                );\n              })}\n            {reference && data.length > 0 && (\n              <motion.span\n                className={cn(\"absolute flex whitespace-nowrap leading-[13px]\", vertical ? \"-translate-y-1/2 flex-col pl-2.5\" : \"-translate-x-1/2 gap-1\")}\n                style={vertical ? { left: width - m.right } : { top: 2 }}\n                initial={false}\n                animate={vertical ? { top: scale(reference.value) } : { left: scale(reference.value) }}\n                transition={{ duration: reduce ? 0 : 0.45, ease: ease.inOut }}\n              >\n                <span className=\"text-fg-2\">{reference.label}</span>\n                <span className=\"text-fg-3\">{fmt(reference.value)}</span>\n              </motion.span>\n            )}\n          </div>\n\n          {(empty || skeleton) && (\n            <div\n              className=\"absolute grid place-items-center text-[12.5px] text-fg-3\"\n              style={{ left: m.left, top: m.top, width: plotW, height: plotH }}\n              role={empty ? \"status\" : undefined}\n            >\n              {empty ? emptyLabel : <span className=\"sr-only\">Loading chart</span>}\n            </div>\n          )}\n\n          <AnimatePresence>\n            {tipVisible && active != null && (\n              <motion.div\n                key=\"tip\"\n                id={tooltipId}\n                role=\"tooltip\"\n                className=\"pointer-events-none absolute z-(--z-tooltip)\"\n                style={{ left: tipLeft, top: tipTop, x: tipShift, y: vertical ? 0 : \"-100%\", transformOrigin: vertical ? undefined : \"bottom center\" }}\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-[132px] rounded-lg border border-line-2 bg-raised px-2.5 py-2 shadow-pop\">\n                  <div className=\"mb-1.5 text-[11.5px] text-fg-2\">{catLabel(active)}</div>\n                  <div className=\"flex flex-col gap-1\">\n                    {series.map((s, si) => (\n                      <div key={s.key} className=\"flex items-center gap-2 text-[12px]\">\n                        <span className={cn(\"size-2 shrink-0 rounded-[2px] bg-current\", s.className ?? \"text-fg\")} style={{ opacity: inkOf(si) }} />\n                        <span className=\"min-w-0 flex-1 truncate text-fg-2\">{s.label}</span>\n                        <span className=\"pl-3 font-medium text-fg tabular\">{fmt(bars[active][si].value)}</span>\n                      </div>\n                    ))}\n                    {stacked && series.length > 1 && (\n                      <div className=\"mt-0.5 flex items-center gap-2 border-t border-line pt-1.5 text-[12px]\">\n                        <span className=\"flex-1 text-fg-3\">Total</span>\n                        <span className=\"pl-3 font-medium text-fg tabular\">{fmt(totals[active])}</span>\n                      </div>\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/bar-chart"
}
