{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "accounting-core-gl",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Deferred balance recognizing by future period.",
  "files": [
    {
      "content": "\"use client\";\nimport { axisLabel, niceTicks, vizColorFor } from \"@/lib/ledger/chart-utils\";\nimport { DataTableToggle } from \"@/components/ledger/chart-primitives\";\nimport { Money } from \"@/components/ledger/money\";\n\nexport type RecognitionBucket = {\n  period: string;\n  /** Minor units, by performance obligation. */\n  byObligation: Record<string, number>;\n};\n\nexport interface RevenueWaterfallProps {\n  buckets: RecognitionBucket[];\n  className?: string;\n  currency: string;\n  obligations: string[];\n}\n\n/**\n * The deferred revenue balance recognizing by future period, by performance\n * obligation. The number every SaaS board asks for.\n */\nexport function RevenueWaterfall({ buckets, obligations, currency, className }: RevenueWaterfallProps) {\n  const width = 720;\n  const height = 280;\n  const PAD = { bottom: 40, left: 72, right: 16, top: 16 };\n\n  const totals = buckets.map((b) => obligations.reduce((s, o) => s + (b.byObligation[o] ?? 0), 0));\n  const ticks = niceTicks(0, Math.max(...totals, 1));\n  const max = ticks[ticks.length - 1];\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 / (max || 1)) * plotH;\n  const slot = plotW / buckets.length;\n  const barW = slot * 0.66;\n\n  return (\n    <DataTableToggle\n      className={className}\n      table={\n        <table className=\"w-full text-body\">\n          <caption className=\"sr-only\">Deferred revenue recognition schedule</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              {obligations.map((o) => (\n                <th className=\"py-2 text-right text-label text-text-secondary\" key={o} scope=\"col\">\n                  {o}\n                </th>\n              ))}\n              <th className=\"py-2 text-right text-label text-text-secondary\" scope=\"col\">\n                Total\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            {buckets.map((b, i) => (\n              <tr className=\"border-b border-border-subtle\" key={b.period}>\n                <th className=\"py-2 text-left font-normal\" scope=\"row\">\n                  {b.period}\n                </th>\n                {obligations.map((o) => (\n                  <td className=\"py-2 text-right\" key={o}>\n                    <Money accounting value={{ amount: b.byObligation[o] ?? 0, currency }} />\n                  </td>\n                ))}\n                <td className=\"py-2 text-right\">\n                  <Money accounting value={{ amount: totals[i], currency }} />\n                </td>\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      }\n    >\n      <svg\n        aria-label=\"Deferred revenue recognizing by future period\"\n        className=\"w-full\"\n        role=\"img\"\n        style={{ height }}\n        viewBox={`0 0 ${width} ${height}`}\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        {buckets.map((b, i) => {\n          const x = PAD.left + i * slot + (slot - barW) / 2;\n          let cursor = 0;\n          return (\n            <g key={b.period}>\n              {obligations.map((o) => {\n                const v = b.byObligation[o] ?? 0;\n                const top = y(cursor + v);\n                const h = Math.max(0, y(cursor) - y(cursor + v));\n                cursor += v;\n                return <rect fill={vizColorFor(o)} height={h} key={o} width={barW} x={x} 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={y(totals[i]) - 6}\n              >\n                {axisLabel(totals[i], currency)}\n              </text>\n              <text\n                className=\"numeric\"\n                fill=\"var(--text-tertiary)\"\n                fontSize=\"12\"\n                textAnchor=\"middle\"\n                x={x + barW / 2}\n                y={height - 20}\n              >\n                {b.period}\n              </text>\n            </g>\n          );\n        })}\n      </svg>\n      <ul className=\"mt-3 flex flex-wrap gap-4\">\n        {obligations.map((o) => (\n          <li className=\"flex items-center gap-2 text-caption\" key={o}>\n            <span\n              aria-hidden=\"true\"\n              className=\"size-2.5 rounded-[2px]\"\n              style={{ background: vizColorFor(o) }}\n            />\n            {o}\n          </li>\n        ))}\n      </ul>\n    </DataTableToggle>\n  );\n}\n",
      "path": "components/ledger/revenue-waterfall.tsx",
      "target": "components/ledger/revenue-waterfall.tsx",
      "type": "registry:component"
    }
  ],
  "name": "revenue-waterfall",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/chart-primitives",
    "@lopifi/chart-utils",
    "@lopifi/money"
  ],
  "title": "RevenueWaterfall",
  "type": "registry:component"
}
