{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "dashboard-charts",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "The P&L bridge. Essential and rare.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { axisLabel, domainFor, niceTicks } from \"@/lib/ledger/chart-utils\";\nimport { DataTableToggle, HatchDefs } from \"@/components/ledger/chart-primitives\";\nimport { Money } from \"@/components/ledger/money\";\n\nexport type WaterfallStep = {\n  label: string;\n  /** Minor units. A `subtotal` step is an absolute position, not a delta. */\n  value: number;\n  kind: \"start\" | \"increase\" | \"decrease\" | \"subtotal\" | \"end\";\n};\n\nexport interface WaterfallChartProps {\n  className?: string;\n  currency: string;\n  height?: number;\n  steps: WaterfallStep[];\n  title?: string;\n}\n\nconst PAD = { bottom: 48, left: 72, right: 16, top: 24 };\n\n/**\n * The P&L bridge — how you answer \"why did net income change?\" in one image.\n *\n * Increases, decreases and subtotals are distinguished by position and\n * connector as well as colour, so the chart survives greyscale.\n */\nexport function WaterfallChart({ steps, currency, height = 300, className, title }: WaterfallChartProps) {\n  const uid = React.useId().replace(/:/g, \"\");\n  const width = 720;\n\n  // Running positions, computed here for layout only — the caller owns the numbers.\n  let running = 0;\n  const bars = steps.map((s) => {\n    if (s.kind === \"start\" || s.kind === \"subtotal\" || s.kind === \"end\") {\n      const bar = { from: 0, to: s.value, ...s };\n      running = s.value;\n      return bar;\n    }\n    const from = running;\n    running += s.value;\n    return { from, to: running, ...s };\n  });\n\n  const allValues = bars.flatMap((b) => [b.from, b.to]);\n  const { min, max } = domainFor(allValues);\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 slot = plotW / bars.length;\n  const barW = slot * 0.6;\n\n  return (\n    <DataTableToggle\n      className={className}\n      table={\n        <table className=\"w-full text-body\">\n          <caption className=\"sr-only\">{title ?? \"Waterfall 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                Step\n              </th>\n              <th className=\"py-2 text-right text-label text-text-secondary\" scope=\"col\">\n                Amount\n              </th>\n              <th className=\"py-2 text-right text-label text-text-secondary\" scope=\"col\">\n                Running\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            {bars.map((b) => (\n              <tr className=\"border-b border-border-subtle\" key={b.label}>\n                <th className=\"py-2 text-left font-normal\" scope=\"row\">\n                  {b.label}\n                </th>\n                <td className=\"py-2 text-right\">\n                  <Money accounting value={{ amount: b.value, currency }} />\n                </td>\n                <td className=\"py-2 text-right\">\n                  <Money accounting value={{ amount: b.to, currency }} />\n                </td>\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      }\n    >\n      <svg\n        aria-label={title ?? \"Waterfall chart\"}\n        className=\"w-full\"\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        <line stroke=\"var(--border-strong)\" x1={PAD.left} x2={width - PAD.right} y1={y(0)} y2={y(0)} />\n\n        {bars.map((b, i) => {\n          const x = PAD.left + i * slot + (slot - barW) / 2;\n          const top = y(Math.max(b.from, b.to));\n          const h = Math.max(2, Math.abs(y(b.from) - y(b.to)));\n          const neutral = b.kind === \"start\" || b.kind === \"subtotal\" || b.kind === \"end\";\n          const fill = neutral\n            ? \"var(--text-secondary)\"\n            : b.kind === \"increase\"\n              ? \"var(--positive)\"\n              : `url(#hatch-${uid})`;\n\n          return (\n            <g key={b.label}>\n              {/* The connector is a non-chromatic cue for direction. */}\n              {i > 0 ? (\n                <line\n                  stroke=\"var(--border-strong)\"\n                  strokeDasharray=\"3 3\"\n                  x1={PAD.left + (i - 1) * slot + (slot + barW) / 2}\n                  x2={x}\n                  y1={y(bars[i - 1].to)}\n                  y2={y(bars[i - 1].to)}\n                />\n              ) : null}\n              <rect\n                fill={fill}\n                height={h}\n                rx={barW < 12 ? 0 : 4}\n                stroke={b.kind === \"decrease\" ? \"var(--negative)\" : \"none\"}\n                width={barW}\n                x={x}\n                y={top}\n              />\n              <text\n                className=\"numeric\"\n                fill=\"var(--text-secondary)\"\n                fontSize=\"11\"\n                textAnchor=\"middle\"\n                x={x + barW / 2}\n                y={top - 6}\n              >\n                {b.kind === \"increase\" ? \"+\" : b.kind === \"decrease\" ? \"−\" : \"\"}\n                {axisLabel(Math.abs(b.value), currency)}\n              </text>\n              <text\n                fill=\"var(--text-secondary)\"\n                fontSize=\"11\"\n                textAnchor=\"middle\"\n                x={x + barW / 2}\n                y={height - 28}\n              >\n                {b.label.length > 14 ? `${b.label.slice(0, 13)}…` : b.label}\n              </text>\n            </g>\n          );\n        })}\n      </svg>\n    </DataTableToggle>\n  );\n}\n",
      "path": "components/ledger/waterfall-chart.tsx",
      "target": "components/ledger/waterfall-chart.tsx",
      "type": "registry:component"
    }
  ],
  "name": "waterfall-chart",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/chart-primitives",
    "@lopifi/chart-utils",
    "@lopifi/money"
  ],
  "title": "WaterfallChart",
  "type": "registry:component"
}
