{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "dashboard-charts",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Grouped/stacked, zero-baseline origin, hatched negatives.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useInViewOnce } from \"@/hooks/ledger/use-in-view\";\nimport { useReducedMotion } from \"@/hooks/ledger/use-media-query\";\nimport { axisLabel, domainFor, niceTicks, vizColorFor } from \"@/lib/ledger/chart-utils\";\nimport { ChartLegend, DataTableToggle, HatchDefs } from \"@/components/ledger/chart-primitives\";\nimport { Money } from \"@/components/ledger/money\";\n\nexport type BarSeries = { key: string; label: string; values: number[] };\n\nexport interface MoneyBarChartProps {\n  className?: string;\n  currency: string;\n  height?: number;\n  labels: string[];\n  mode?: \"grouped\" | \"stacked\";\n  series: BarSeries[];\n  title?: string;\n}\n\nconst PAD = { bottom: 28, left: 64, right: 16, top: 16 };\n\n/**\n * `transform-origin` sits at the zero baseline, not at the bottom — otherwise\n * a negative bar grows upward from the floor and briefly displays the wrong\n * sign, which is a correctness issue inside an animation.\n *\n * Bar radius 4 on the outer corners only, square below 12px width.\n */\nexport function MoneyBarChart({\n  labels,\n  series,\n  currency,\n  mode = \"grouped\",\n  height = 260,\n  className,\n  title,\n}: MoneyBarChartProps) {\n  const [isolated, setIsolated] = React.useState<string | null>(null);\n  const reduced = useReducedMotion();\n  /* The bars grow when the chart is scrolled to, not when it mounts. A chart\n     below the fold used to play its whole 400ms while nobody was looking and\n     was fully grown by the time the reader got there — the animation existed\n     and was never once seen. Once, and never again on the way back up. */\n  const [viewRef, inView] = useInViewOnce<SVGSVGElement>();\n  const uid = React.useId().replace(/:/g, \"\");\n  const width = 720;\n\n  const visible = isolated ? series.filter((s) => s.key === isolated) : series;\n  const stackedTotals = labels.map((_, i) => visible.reduce((sum, s) => sum + s.values[i], 0));\n  const domainValues = mode === \"stacked\" ? stackedTotals : visible.flatMap((s) => s.values);\n  const { min, max } = domainFor(domainValues.length ? domainValues : [0]);\n  const ticks = niceTicks(min, max);\n  const domainMin = Math.min(min, ticks[0]);\n  const domainMax = Math.max(max, ticks[ticks.length - 1]);\n\n  const plotW = width - PAD.left - PAD.right;\n  const plotH = height - PAD.top - PAD.bottom;\n  const y = (v: number) => PAD.top + plotH - ((v - domainMin) / (domainMax - domainMin || 1)) * plotH;\n  const zeroY = y(0);\n\n  const groupWidth = plotW / labels.length;\n  const innerPad = groupWidth * 0.2;\n  const barWidth =\n    mode === \"stacked\" ? groupWidth - innerPad : (groupWidth - innerPad) / Math.max(1, visible.length);\n  const radius = barWidth < 12 ? 0 : 4;\n\n  return (\n    <DataTableToggle\n      className={className}\n      table={\n        <table className=\"w-full text-body\">\n          <caption className=\"sr-only\">{title ?? \"Chart data\"}</caption>\n          <thead>\n            <tr className=\"border-b border-border-subtle\">\n              <th className=\"py-2 text-left text-label text-text-secondary\" scope=\"col\">\n                Period\n              </th>\n              {series.map((s) => (\n                <th className=\"py-2 text-right text-label text-text-secondary\" key={s.key} scope=\"col\">\n                  {s.label}\n                </th>\n              ))}\n            </tr>\n          </thead>\n          <tbody>\n            {labels.map((label, i) => (\n              <tr className=\"border-b border-border-subtle\" key={label}>\n                <th className=\"py-2 text-left font-normal\" scope=\"row\">\n                  {label}\n                </th>\n                {series.map((s) => (\n                  <td className=\"py-2 text-right\" key={s.key}>\n                    <Money accounting value={{ amount: s.values[i], currency }} />\n                  </td>\n                ))}\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      }\n    >\n      <svg\n        aria-label={title ?? \"Bar chart\"}\n        className=\"w-full\"\n        ref={viewRef}\n        role=\"img\"\n        style={{ height }}\n        viewBox={`0 0 ${width} ${height}`}\n      >\n        <HatchDefs id={`hatch-${uid}`} />\n\n        {ticks.map((t) => (\n          <g key={t}>\n            <line stroke=\"var(--border-subtle)\" x1={PAD.left} x2={width - PAD.right} y1={y(t)} y2={y(t)} />\n            <text\n              className=\"numeric\"\n              fill=\"var(--text-tertiary)\"\n              fontSize=\"12\"\n              textAnchor=\"end\"\n              x={PAD.left - 8}\n              y={y(t) + 4}\n            >\n              {axisLabel(t, currency)}\n            </text>\n          </g>\n        ))}\n\n        <line stroke=\"var(--border-strong)\" x1={PAD.left} x2={width - PAD.right} y1={zeroY} y2={zeroY} />\n\n        {labels.map((label, i) => {\n          const groupX = PAD.left + i * groupWidth + innerPad / 2;\n          let positiveCursor = 0;\n          let negativeCursor = 0;\n\n          return (\n            <g key={label}>\n              {visible.map((s, si) => {\n                const v = s.values[i];\n                const negative = v < 0;\n                let top: number;\n                let bottom: number;\n\n                if (mode === \"stacked\") {\n                  const base = negative ? negativeCursor : positiveCursor;\n                  const next = base + v;\n                  if (negative) negativeCursor = next;\n                  else positiveCursor = next;\n                  top = y(Math.max(base, next));\n                  bottom = y(Math.min(base, next));\n                } else {\n                  top = y(Math.max(0, v));\n                  bottom = y(Math.min(0, v));\n                }\n\n                const barX = mode === \"stacked\" ? groupX : groupX + si * barWidth;\n                const h = Math.max(1, bottom - top);\n\n                return (\n                  <rect\n                    fill={negative ? `url(#hatch-${uid})` : vizColorFor(s.key)}\n                    height={h}\n                    key={s.key}\n                    rx={radius}\n                    stroke={negative ? \"var(--negative)\" : \"none\"}\n                    strokeWidth={negative ? 1 : 0}\n                    style={\n                      reduced\n                        ? undefined\n                        : {\n                            animation: inView\n                              ? `ledger-bar-grow 400ms var(--ease-out) ${Math.min(i * 30, 240)}ms both`\n                              : undefined,\n                            /* Held flat until the chart is on screen, so the\n                               growth is the first thing the reader sees rather\n                               than something that finished without them. */\n                            transform: inView ? undefined : \"scaleY(0)\",\n                            transformBox: \"fill-box\",\n                            // Grow from the zero baseline, never from the floor.\n                            transformOrigin: negative ? \"center top\" : \"center bottom\",\n                          }\n                    }\n                    width={Math.max(1, barWidth - 2)}\n                    x={barX}\n                    y={top}\n                  />\n                );\n              })}\n              {mode === \"stacked\" ? (\n                <text\n                  className=\"numeric\"\n                  fill=\"var(--text-secondary)\"\n                  fontSize=\"11\"\n                  textAnchor=\"middle\"\n                  x={groupX + barWidth / 2}\n                  y={y(stackedTotals[i]) - 6}\n                >\n                  {axisLabel(stackedTotals[i], currency)}\n                </text>\n              ) : null}\n              <text\n                className=\"numeric\"\n                fill=\"var(--text-tertiary)\"\n                fontSize=\"12\"\n                textAnchor=\"middle\"\n                x={groupX + (mode === \"stacked\" ? barWidth : barWidth * visible.length) / 2}\n                y={height - 8}\n              >\n                {label}\n              </text>\n            </g>\n          );\n        })}\n      </svg>\n\n      {series.length > 1 ? (\n        <ChartLegend\n          className=\"mt-3\"\n          isolated={isolated}\n          items={series.map((s) => ({ color: vizColorFor(s.key), key: s.key, label: s.label }))}\n          onIsolate={setIsolated}\n        />\n      ) : null}\n\n      <style>\n        {\"@keyframes ledger-bar-grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }\"}\n      </style>\n    </DataTableToggle>\n  );\n}\n",
      "path": "components/ledger/money-bar-chart.tsx",
      "target": "components/ledger/money-bar-chart.tsx",
      "type": "registry:component"
    }
  ],
  "name": "money-bar-chart",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/chart-primitives",
    "@lopifi/chart-utils",
    "@lopifi/money",
    "@lopifi/use-in-view",
    "@lopifi/use-media-query"
  ],
  "title": "MoneyBarChart",
  "type": "registry:component"
}
