{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-bar",
  "title": "Chart Bar",
  "description": "Grouped or stacked bar chart, vertical or horizontal, with springy staggered growth, textured series, hover highlight and a toggleable legend.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://trytweenly.vercel.app/r/chart-kit.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/chart-bar/chart-bar.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useId, useMemo, useState } from \"react\"\nimport { animate as animateValue, motion, useMotionValue, useMotionValueEvent } from \"motion/react\"\nimport {\n  ChartCard,\n  ChartLegend,\n  ChartPattern,\n  ChartTooltip,\n  chartTheme,\n  createFormatter,\n  niceTicks,\n  resolveSeries,\n  textureFill,\n  toNumber,\n  useChartReveal,\n  type ChartBaseProps,\n  type ChartDatum,\n  type ChartSeries,\n  type ValueFormat,\n} from \"@/registry/new-york/lib/chart-kit\"\n\nexport interface ChartBarProps extends ChartBaseProps {\n  /** Rows of data, e.g. `{ month: \"Jan\", desktop: 186, mobile: 80 }`. */\n  data: ChartDatum[]\n  /** Key in each row used for the category labels. */\n  index: string\n  /** Numeric keys to plot, one bar (or stack segment) each. */\n  series: ChartSeries[]\n  /** \"vertical\" draws columns, \"horizontal\" draws rows. Default: \"vertical\" */\n  layout?: \"vertical\" | \"horizontal\"\n  /** Stack series on top of each other instead of grouping them side by side. Default: false */\n  stacked?: boolean\n  /** Bar corner radius in px. Default: 6 */\n  barRadius?: number\n  /** Gap between bars in a group (or segments in a stack) in px. Default: 4 */\n  barGap?: number\n  /** Share of each category slot left empty, 0 – 0.8. Default: 0.3 */\n  categoryGap?: number\n  /** Grid lines at each value tick. Default: true */\n  showGrid?: boolean\n  /** Bottom axis labels (categories when vertical, values when horizontal). Default: true */\n  showXAxis?: boolean\n  /** Left axis labels (values when vertical, categories when horizontal). Default: true */\n  showYAxis?: boolean\n  /** Approximate number of value-axis ticks. Default: 4 */\n  yTicks?: number\n  /** Value labels at the end of each bar (each stack when stacked). Default: false */\n  showValues?: boolean\n  /** \"hover\" dims the other categories while one is hovered. Default: \"hover\" */\n  highlight?: \"hover\" | \"none\"\n  /** Tooltip on hover / arrow keys. Default: true */\n  showTooltip?: boolean\n  /** Toggleable legend. Default: true when there is more than one series */\n  showLegend?: boolean\n  /** Plot height in px, excluding the bottom axis. Default: 220 */\n  height?: number\n  /** Headline figure from the first series. Default: \"sum\" */\n  total?: \"sum\" | \"last\" | \"none\"\n  /** Change pill next to the headline, in percent. \"auto\" compares the first and last value of the first series. Default: \"auto\" */\n  delta?: number | \"auto\"\n}\n\ninterface Bar {\n  key: string\n  si: number\n  ci: number\n  on: boolean\n  x: number\n  y: number\n  w: number\n  h: number\n  start: { x: number; y: number; w: number; h: number }\n}\n\nexport function ChartBar({\n  data,\n  index,\n  series,\n  layout = \"vertical\",\n  stacked = false,\n  barRadius = 6,\n  barGap = 4,\n  categoryGap = 0.3,\n  showGrid = true,\n  showXAxis = true,\n  showYAxis = true,\n  yTicks = 4,\n  showValues = false,\n  highlight = \"hover\",\n  showTooltip = true,\n  showLegend,\n  height = 220,\n  total = \"sum\",\n  delta = \"auto\",\n  title,\n  description,\n  titleSize,\n  surface = \"dark\",\n  accent,\n  palette,\n  icon,\n  radius,\n  bare,\n  animate: animated = true,\n  duration = 1.2,\n  delay = 0,\n  once = true,\n  valueFormat = \"number\",\n  currency = \"USD\",\n  decimals,\n  className,\n}: ChartBarProps) {\n  const theme = chartTheme(surface, accent)\n  const resolved = resolveSeries(series, theme, palette)\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\")\n  const { ref, shown, still } = useChartReveal(once, animated)\n  const width = useWidth(ref)\n  const [hidden, setHidden] = useState<Set<string>>(() => new Set())\n  const [active, setActive] = useState<number | null>(null)\n  const [settled, setSettled] = useState(false)\n\n  const fmt = useMemo(() => createFormatter(valueFormat, { currency, decimals }), [valueFormat, currency, decimals])\n  const axisFmt = useMemo(() => axisFormatter(valueFormat, currency, decimals), [valueFormat, currency, decimals])\n\n  const horizontal = layout === \"horizontal\"\n  const n = data.length\n  const visible = resolved.filter((s) => !hidden.has(s.key))\n  const catStagger = n > 1 ? (duration * 0.45) / (n - 1) : 0\n  const segDur = stacked ? duration * 0.55 : duration * 0.8\n  const entranceEnd = delay + (n - 1) * catStagger + segDur * (stacked ? visible.length : 1.3)\n\n  useEffect(() => {\n    if (!shown || still) return\n    const id = setTimeout(() => setSettled(true), entranceEnd * 1000 + 100)\n    return () => clearTimeout(id)\n  }, [shown, still, entranceEnd])\n\n  let lo = 0\n  let hi = 0\n  for (const row of data) {\n    let pos = 0\n    let neg = 0\n    for (const s of visible) {\n      const v = toNumber(row[s.key])\n      if (stacked) {\n        if (v >= 0) pos += v\n        else neg += v\n      } else {\n        hi = Math.max(hi, v)\n        lo = Math.min(lo, v)\n      }\n    }\n    if (stacked) {\n      hi = Math.max(hi, pos)\n      lo = Math.min(lo, neg)\n    }\n  }\n  if (hi === lo) hi = lo + 1\n  const ticks = niceTicks(lo, hi, yTicks)\n  const dLo = ticks[0]\n  const dHi = ticks[ticks.length - 1]\n\n  const cats = data.map((d) => String(d[index] ?? \"\"))\n  const longestCat = Math.max(...cats.map((c) => c.length), 1)\n  const longestTick = Math.max(...ticks.map((t) => axisFmt(t).length), 1)\n  const valueLabelW = showValues ? longestTick * 6 + 10 : 0\n\n  const padT = showValues && !horizontal ? 18 : 8\n  const padR = horizontal ? Math.max(valueLabelW, 12) : 4\n  const padL = showYAxis ? (horizontal ? longestCat : longestTick) * 6.4 + 14 : horizontal ? 2 : 4\n  const svgH = height + (showXAxis ? 26 : 0) + (showValues && !horizontal && dLo < 0 ? 14 : 0)\n  const plotW = Math.max(width - padL - padR, 1)\n  const plotH = Math.max(height - padT, 1)\n\n  const valueAt = (v: number) =>\n    horizontal ? padL + ((v - dLo) / (dHi - dLo)) * plotW : height - ((v - dLo) / (dHi - dLo)) * plotH\n  const zero = valueAt(Math.min(Math.max(0, dLo), dHi))\n  const bandLen = horizontal ? plotH : plotW\n  const bandOrigin = horizontal ? padT : padL\n  const step = bandLen / Math.max(n, 1)\n  const inner = step * (1 - Math.min(Math.max(categoryGap, 0), 0.8))\n  const bandStart = (i: number) => bandOrigin + i * step + (step - inner) / 2\n\n  const k = Math.max(visible.length, 1)\n  const barW = stacked ? inner : Math.max((inner - barGap * (k - 1)) / k, 1)\n\n  const rect = (pos: number, size: number, a: number, b: number) => {\n    const v0 = Math.min(a, b)\n    const len = Math.abs(a - b)\n    return horizontal ? { x: v0, y: pos, w: len, h: size } : { x: pos, y: v0, w: size, h: len }\n  }\n\n  const bars: Bar[] = []\n  data.forEach((row, ci) => {\n    let slot = 0\n    let pos = 0\n    let neg = 0\n    resolved.forEach((s, si) => {\n      const on = !hidden.has(s.key)\n      const v = on ? toNumber(row[s.key]) : 0\n      let p: number\n      let size: number\n      let a: number\n      let b: number\n      if (stacked) {\n        p = bandStart(ci)\n        size = inner\n        const from = v >= 0 ? pos : neg\n        const to = from + v\n        if (v >= 0) pos = to\n        else neg = to\n        a = valueAt(from)\n        b = valueAt(to)\n        const dir = Math.sign(b - a)\n        const trim = Math.min(barGap / 2, Math.abs(b - a) / 2)\n        if (from !== 0) a += dir * trim\n        b -= dir * trim\n      } else {\n        p = bandStart(ci) + slot * (barW + barGap) - (on ? 0 : barGap / 2)\n        size = on ? barW : 0\n        a = zero\n        b = valueAt(v)\n      }\n      if (on) slot++\n      bars.push({ key: `${s.key}-${ci}`, si, ci, on, ...rect(p, size, a, b), start: rect(p, size, a, a) })\n    })\n  })\n\n  const firstVals = series[0] ? data.map((d) => toNumber(d[series[0].key])) : []\n  const first = firstVals[0] ?? 0\n  const last = firstVals[firstVals.length - 1] ?? 0\n  const headline =\n    total === \"none\" || !firstVals.length ? undefined : total === \"sum\" ? firstVals.reduce((x, y) => x + y, 0) : last\n  const deltaValue =\n    delta === \"auto\" ? (firstVals.length > 1 && first !== 0 ? ((last - first) / Math.abs(first)) * 100 : undefined) : delta\n\n  const legend = showLegend ?? series.length > 1\n  const tracking = n > 0 && (showTooltip || highlight === \"hover\")\n  const activeIdx = active !== null && active < n ? active : null\n  const quick = still || settled\n\n  const toggle = (key: string) =>\n    setHidden((prev) => {\n      const next = new Set(prev)\n      if (next.has(key)) next.delete(key)\n      else next.add(key)\n      return next\n    })\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    if (!tracking) return\n    const fwd = horizontal ? \"ArrowDown\" : \"ArrowRight\"\n    const back = horizontal ? \"ArrowUp\" : \"ArrowLeft\"\n    const moves: Record<string, (a: number | null) => number | null> = {\n      [fwd]: (a) => (a === null ? 0 : Math.min(n - 1, a + 1)),\n      [back]: (a) => (a === null ? n - 1 : Math.max(0, a - 1)),\n      Home: () => 0,\n      End: () => n - 1,\n      Escape: () => null,\n    }\n    const move = moves[e.key]\n    if (!move) return\n    e.preventDefault()\n    setActive(move)\n  }\n\n  const growth = (b: Bar) =>\n    still\n      ? { duration: 0 }\n      : quick\n        ? { type: \"spring\" as const, duration: 0.6, bounce: 0.15 }\n        : {\n            type: \"spring\" as const,\n            duration: segDur,\n            bounce: stacked ? 0.15 : 0.3,\n            delay: delay + b.ci * catStagger + (stacked ? visible.filter((s) => resolved.indexOf(s) < b.si).length * segDur * 0.7 : b.si * 0.06),\n          }\n\n  const labelFor = (ci: number) => {\n    const group = bars.filter((b) => b.ci === ci && b.on)\n    if (!group.length) return null\n    const row = data[ci]\n    if (stacked) {\n      const sum = visible.reduce((acc, s) => acc + toNumber(row[s.key]), 0)\n      const ends = group.map((b) => (horizontal ? b.x + b.w : b.y))\n      const end = horizontal ? Math.max(...ends) : Math.min(...ends)\n      const c = bandStart(ci) + inner / 2\n      return [{ key: `t-${ci}`, text: axisFmt(sum), x: horizontal ? end + 6 : c, y: horizontal ? c : end - 6, neg: false, delay: entranceEnd }]\n    }\n    return group.map((b) => {\n      const v = toNumber(row[resolved[b.si].key])\n      const neg = v < 0\n      return horizontal\n        ? { key: b.key, text: axisFmt(v), x: neg ? b.x - 6 : b.x + b.w + 6, y: b.y + b.h / 2, neg, delay: delay + b.ci * catStagger + segDur * 0.6 }\n        : { key: b.key, text: axisFmt(v), x: b.x + b.w / 2, y: neg ? b.y + b.h + 13 : b.y - 6, neg, delay: delay + b.ci * catStagger + segDur * 0.6 }\n    })\n  }\n\n  const activeBars = activeIdx === null ? [] : bars.filter((b) => b.ci === activeIdx && b.on)\n  const bandCenter = activeIdx === null ? 0 : bandStart(activeIdx) + inner / 2\n  let tipX = 0\n  let tipY = 0\n  if (activeIdx !== null) {\n    if (horizontal) {\n      tipX = activeBars.length ? Math.max(...activeBars.map((b) => b.x + b.w)) : zero\n      tipY = bandCenter\n    } else {\n      const flip = bandCenter > width * 0.6\n      tipX = flip ? bandStart(activeIdx) : bandStart(activeIdx) + inner\n      const top = activeBars.length ? Math.min(...activeBars.map((b) => b.y)) : zero\n      tipY = Math.min(Math.max(top + 24, 40), height - 40)\n    }\n  }\n\n  const catEvery = horizontal\n    ? Math.max(1, Math.ceil((n * 16) / plotH))\n    : Math.max(1, Math.ceil((n * (longestCat * 6.6 + 12)) / plotW))\n  const fade = (d: number) => (still ? { duration: 0 } : { duration: 0.5, delay: quick ? 0 : d })\n  const morph = still ? { duration: 0 } : { type: \"spring\" as const, duration: 0.6, bounce: 0.1 }\n\n  return (\n    <ChartCard\n      theme={theme}\n      title={title}\n      description={description}\n      titleSize={titleSize}\n      value={\n        headline === undefined ? undefined : (\n          <Ticker value={headline} format={fmt} shown={shown} still={still} delay={delay} duration={duration} />\n        )\n      }\n      delta={headline === undefined ? undefined : deltaValue}\n      icon={icon === undefined ? <BarsGlyph /> : icon}\n      radius={radius}\n      bare={bare}\n      className={className}\n    >\n      <div\n        ref={ref}\n        tabIndex={tracking ? 0 : undefined}\n        role=\"group\"\n        aria-label={title ? `${title} chart` : \"Bar chart\"}\n        onKeyDown={onKeyDown}\n        onBlur={() => setActive(null)}\n        className=\"relative w-full touch-pan-y select-none rounded-md outline-offset-4 focus-visible:outline-2\"\n        style={{ height: svgH, outlineColor: theme.accent }}\n      >\n        {width > 0 && n > 0 && (\n          <svg width={width} height={svgH} className=\"block overflow-visible\" onPointerLeave={() => setActive(null)}>\n            <defs>\n              {resolved.map((s) => (\n                <ChartPattern key={s.key} id={`${uid}-p-${s.key}`} color={s.color} texture={s.texture} theme={theme} />\n              ))}\n            </defs>\n\n            {tracking && (\n              <motion.rect\n                rx={Math.min(barRadius + 4, 12)}\n                fill={theme.fg}\n                pointerEvents=\"none\"\n                initial={false}\n                animate={\n                  horizontal\n                    ? { x: padL - 4, width: plotW + 8, y: bandStart(activeIdx ?? 0) - 5, height: inner + 10, opacity: activeIdx === null ? 0 : 0.045 }\n                    : { y: padT - 6, height: height - padT + 6, x: bandStart(activeIdx ?? 0) - 5, width: inner + 10, opacity: activeIdx === null ? 0 : 0.045 }\n                }\n                transition={still ? { duration: 0 } : { type: \"spring\", stiffness: 600, damping: 45, opacity: { duration: 0.15 } }}\n              />\n            )}\n\n            {showGrid &&\n              ticks.map((v) => {\n                const p = valueAt(v)\n                const isZero = v === 0 && dLo < 0\n                return (\n                  <motion.line\n                    key={v}\n                    stroke={isZero ? theme.subtle : theme.grid}\n                    strokeDasharray={isZero ? undefined : \"3 4\"}\n                    initial={still ? false : horizontal ? { opacity: 0, x1: p, x2: p, y1: padT, y2: height } : { opacity: 0, y1: p, y2: p, x1: padL, x2: width - padR }}\n                    animate={{ opacity: shown ? 1 : 0, ...(horizontal ? { x1: p, x2: p, y1: padT, y2: height } : { y1: p, y2: p, x1: padL, x2: width - padR }) }}\n                    transition={{ ...morph, opacity: fade(delay) }}\n                  />\n                )\n              })}\n\n            {(horizontal ? showXAxis : showYAxis) &&\n              ticks.map((v) => {\n                const p = valueAt(v)\n                return (\n                  <motion.text\n                    key={v}\n                    fontSize={11}\n                    fill={theme.muted}\n                    className=\"tabular-nums\"\n                    textAnchor={horizontal ? \"middle\" : \"end\"}\n                    dominantBaseline={horizontal ? \"auto\" : \"middle\"}\n                    initial={still ? false : horizontal ? { opacity: 0, x: p, y: height + 19 } : { opacity: 0, x: padL - 10, y: p }}\n                    animate={{ opacity: shown ? 1 : 0, ...(horizontal ? { x: p, y: height + 19 } : { x: padL - 10, y: p }) }}\n                    transition={{ ...morph, opacity: fade(delay + 0.1) }}\n                  >\n                    {axisFmt(v)}\n                  </motion.text>\n                )\n              })}\n\n            {(horizontal ? showYAxis : showXAxis) &&\n              cats.map((c, i) => {\n                if (i % catEvery !== 0) return null\n                const center = bandStart(i) + inner / 2\n                return (\n                  <motion.text\n                    key={i}\n                    fontSize={11}\n                    fill={activeIdx === i ? theme.fg : theme.muted}\n                    textAnchor={horizontal ? \"end\" : \"middle\"}\n                    dominantBaseline={horizontal ? \"middle\" : \"auto\"}\n                    initial={still ? false : { opacity: 0, x: horizontal ? padL - 10 : center, y: horizontal ? center : height + 19 }}\n                    animate={{ opacity: shown ? 1 : 0, x: horizontal ? padL - 10 : center, y: horizontal ? center : height + 19 }}\n                    transition={{ ...morph, opacity: fade(delay + i * catStagger) }}\n                  >\n                    {c}\n                  </motion.text>\n                )\n              })}\n\n            {bars.map((b) => {\n              const s = resolved[b.si]\n              const dim = highlight === \"hover\" && activeIdx !== null && activeIdx !== b.ci\n              const target = shown ? b : b.start\n              const grow = growth(b)\n              const growDelay = \"delay\" in grow ? grow.delay : 0\n              return (\n                <motion.rect\n                  key={b.key}\n                  rx={barRadius}\n                  fill={textureFill(`${uid}-p-${s.key}`, s.texture, s.color)}\n                  stroke={s.texture === \"solid\" ? undefined : s.texture === \"muted\" ? theme.muted : s.color}\n                  strokeOpacity={0.55}\n                  strokeWidth={s.texture === \"solid\" ? 0 : 1}\n                  pointerEvents=\"none\"\n                  initial={still ? false : { x: b.start.x, y: b.start.y, width: b.start.w, height: b.start.h, opacity: 0 }}\n                  animate={{\n                    x: target.x,\n                    y: target.y,\n                    width: target.w,\n                    height: target.h,\n                    opacity: !b.on || !shown ? 0 : dim ? 0.3 : 1,\n                  }}\n                  transition={{\n                    ...grow,\n                    opacity: still ? { duration: 0 } : { duration: 0.2, delay: quick || !b.on ? 0 : growDelay },\n                  }}\n                />\n              )\n            })}\n\n            {showValues &&\n              data.map((_, ci) =>\n                labelFor(ci)?.map((l) => (\n                  <motion.text\n                    key={l.key}\n                    fontSize={10.5}\n                    fontWeight={500}\n                    fill={theme.muted}\n                    className=\"tabular-nums\"\n                    pointerEvents=\"none\"\n                    textAnchor={horizontal ? (l.neg ? \"end\" : \"start\") : \"middle\"}\n                    dominantBaseline={horizontal ? \"middle\" : \"auto\"}\n                    initial={still ? false : { opacity: 0, x: l.x, y: l.y }}\n                    animate={{ opacity: shown ? (highlight === \"hover\" && activeIdx !== null && activeIdx !== ci ? 0.35 : 1) : 0, x: l.x, y: l.y }}\n                    transition={{ ...morph, opacity: still ? { duration: 0 } : { duration: 0.3, delay: quick ? 0 : l.delay } }}\n                  >\n                    {l.text}\n                  </motion.text>\n                ))\n              )}\n\n            {tracking &&\n              data.map((_, i) => (\n                <rect\n                  key={i}\n                  fill=\"transparent\"\n                  onPointerEnter={() => setActive(i)}\n                  {...(horizontal\n                    ? { x: padL, width: plotW + padR, y: bandOrigin + i * step, height: step }\n                    : { y: 0, height: svgH, x: bandOrigin + i * step, width: step })}\n                />\n              ))}\n          </svg>\n        )}\n\n        <ChartTooltip\n          open={showTooltip && activeIdx !== null && visible.length > 0}\n          x={tipX}\n          y={tipY}\n          width={width}\n          title={activeIdx === null ? undefined : cats[activeIdx]}\n          rows={\n            activeIdx === null\n              ? []\n              : visible.map((s) => ({ label: s.label, value: fmt(toNumber(data[activeIdx]?.[s.key])), color: s.color, texture: s.texture }))\n          }\n          theme={theme}\n        />\n      </div>\n\n      {legend && (\n        <div className=\"mt-4\">\n          <ChartLegend series={resolved} theme={theme} hidden={hidden} onToggle={toggle} patternPrefix={`${uid}-p`} />\n        </div>\n      )}\n    </ChartCard>\n  )\n}\n\nfunction useWidth(ref: React.RefObject<HTMLElement | null>) {\n  const [width, setWidth] = useState(0)\n  useEffect(() => {\n    const el = ref.current\n    if (!el) return\n    const ro = new ResizeObserver(([entry]) => setWidth(Math.round(entry.contentRect.width)))\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [ref])\n  return width\n}\n\nfunction axisFormatter(format: ValueFormat, currency: string, decimals?: number) {\n  if (typeof format === \"function\") return format\n  if (format === \"currency\") {\n    const intl = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency, notation: \"compact\", maximumFractionDigits: 1 })\n    return (v: number) => intl.format(v)\n  }\n  return createFormatter(format === \"percent\" ? \"percent\" : \"compact\", { decimals })\n}\n\nfunction Ticker({ value, format, shown, still, delay, duration }: {\n  value: number\n  format: (v: number) => string\n  shown: boolean\n  still: boolean\n  delay: number\n  duration: number\n}) {\n  const mv = useMotionValue(still ? value : 0)\n  const [display, setDisplay] = useState(still ? value : 0)\n  useMotionValueEvent(mv, \"change\", setDisplay)\n  useEffect(() => {\n    if (!shown) return\n    if (still) {\n      mv.set(value)\n      return\n    }\n    const controls = animateValue(mv, value, { duration, delay, ease: [0.16, 1, 0.3, 1] })\n    return () => controls.stop()\n  }, [mv, value, shown, still, delay, duration])\n  return <span>{format(display)}</span>\n}\n\nfunction BarsGlyph() {\n  return (\n    <svg viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\">\n      <path d=\"M3 13V8M8 13V3M13 13V6\" />\n    </svg>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/chart-bar",
  "type": "registry:component"
}