{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "metrics-analysis",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Revenue by stream, with mix, growth and concentration.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { compoundGrowth, periodChange, runRate, shareOfTotal, trailingAverage } from \"@/lib/ledger/period-change\";\nimport { InfoTip } from \"@/components/ledger/info-tip\";\nimport { Money } from \"@/components/ledger/money\";\nimport { Percent } from \"@/components/ledger/percent\";\nimport type { PeriodColumn, PeriodRow } from \"@/components/ledger/period-grid\";\nimport { PeriodGrid } from \"@/components/ledger/period-grid\";\n\nexport interface RevenueStream {\n  /** \"Subscription\", \"Services\", \"Usage\" — grouped in the mix breakdown. */\n  category?: string;\n  id: string;\n  label: string;\n  /** 1–3. A stream, a product inside it, a plan inside that. */\n  level?: number;\n  /** One entry per period. `null` is \"not posted\", never 0. */\n  values: (number | null)[];\n}\n\nexport interface RevenueTrackerProps {\n  /** Index of the last posted period. Everything after it is a projection. */\n  actualThrough?: number;\n  /** \"Recognised revenue, accrual basis\" — stated, never assumed. */\n  basis?: string;\n  className?: string;\n  currency: string;\n  /** Resolved period labels, oldest first. */\n  periods: string[];\n  /** Prior-year figures for the same periods, if the entity is old enough. */\n  priorYear?: (number | null)[];\n  storageKey?: string;\n  streams: RevenueStream[];\n}\n\n/**\n * Revenue by stream, period by period, with the four questions an operator\n * actually asks about it.\n *\n * The grid is the component; the strip above it is a summary of the grid's\n * last posted column, and every figure in it is computed from the same array\n * the cells are drawn from. There is no second data path — a header KPI that\n * disagrees with the table beneath it is the fastest way to lose a reader.\n *\n * The four questions, in the order they get asked:\n *\n * 1. **How much, this period.** The total of the last posted column.\n * 2. **Versus what.** Month on month, and against the same period last year\n *    when there is a prior year to compare to. Both, because a month-on-month\n *    read of a seasonal business is noise, and a year-on-year read of a fast\n *    one is stale.\n * 3. **From where.** The mix. `% of` in the grid's own view switch turns\n *    every cell into its share of total revenue for that column.\n * 4. **How concentrated.** One stream over a third of revenue is a fact about\n *    the risk in the business, and it is flagged rather than left to be\n *    noticed.\n *\n * What it will not do: annualise a month and call it ARR without saying so.\n * The run-rate tile is labelled a projection from one period, because that is\n * what multiplying a month by twelve produces.\n */\nexport function RevenueTracker({\n  periods,\n  streams,\n  currency,\n  actualThrough,\n  priorYear,\n  storageKey,\n  basis = \"Recognised revenue\",\n  className,\n}: RevenueTrackerProps) {\n  const lastActual = actualThrough ?? periods.length - 1;\n\n  /* The total is computed here rather than accepted as a prop. A \"total\n     revenue\" that is passed in is a number that can disagree with the rows\n     above it, and the first time it does, every figure on the page is in\n     question. Nulls do not contribute and do not zero the total; a column\n     with nothing posted has no total. */\n  const totals = React.useMemo(\n    () =>\n      periods.map((_, i) => {\n        const present = streams.map((s) => s.values[i]).filter((v): v is number => v != null);\n        return present.length ? present.reduce((a, b) => a + b, 0) : null;\n      }),\n    [periods, streams],\n  );\n\n  const columns: PeriodColumn[] = React.useMemo(\n    () =>\n      periods.map((p, i) => ({\n        id: p,\n        kind: i > lastActual ? \"forecast\" : \"actual\",\n        label: p,\n        locked: i === lastActual,\n      })),\n    [periods, lastActual],\n  );\n\n  const rows: PeriodRow[] = React.useMemo(() => {\n    const streamRows: PeriodRow[] = streams.map((s) => ({\n      id: s.id,\n      label: s.label,\n      level: s.level ?? 1,\n      note: s.category,\n      polarity: \"standard\",\n      values: s.values,\n    }));\n\n    const out: PeriodRow[] = [\n      ...streamRows,\n      {\n        derived: true,\n        id: \"__total\",\n        label: \"Total revenue\",\n        polarity: \"standard\",\n        values: totals,\n        variant: \"total\",\n      },\n      {\n        derived: true,\n        id: \"__growth\",\n        label: \"Growth on prior period\",\n        polarity: \"standard\",\n        unit: \"percent\",\n        values: totals.map((v, i) => {\n          const change = periodChange(v, i > 0 ? totals[i - 1] : null);\n          /* Percent rows are held scaled by 100, like every other figure in\n             the grid — `null` where the ratio does not exist rather than a\n             zero that would read as \"flat\". */\n          return change.relative == null ? null : Math.round(change.relative * 100);\n        }),\n        variant: \"ratio\",\n      },\n    ];\n\n    if (priorYear) {\n      out.push(\n        {\n          derived: true,\n          id: \"__prior-year\",\n          label: \"Same period, prior year\",\n          polarity: \"standard\",\n          values: priorYear,\n          variant: \"ratio\",\n        },\n        {\n          derived: true,\n          id: \"__yoy\",\n          label: \"Growth on prior year\",\n          polarity: \"standard\",\n          unit: \"percent\",\n          values: totals.map((v, i) => {\n            const change = periodChange(v, priorYear[i] ?? null);\n            return change.relative == null ? null : Math.round(change.relative * 100);\n          }),\n          variant: \"ratio\",\n        },\n      );\n    }\n\n    return out;\n  }, [streams, totals, priorYear]);\n\n  /* ── The strip ───────────────────────────────────────────────────── */\n\n  const current = totals[lastActual] ?? null;\n  const mom = periodChange(current, lastActual > 0 ? totals[lastActual - 1] : null);\n  const yoy = periodChange(current, priorYear?.[lastActual] ?? null);\n  const _ttm = trailingAverage(totals.slice(0, lastActual + 1), 12);\n  const projected = runRate(current);\n\n  /* Compound growth across the posted columns — the honest version of \"we are\n     growing X% a month\", which is not the last month's jump. */\n  const cagr = compoundGrowth(totals[0] ?? null, current, Math.max(1, lastActual));\n\n  const concentration = React.useMemo(() => {\n    if (current == null) return null;\n    const ranked = streams\n      .map((s) => ({ label: s.label, value: s.values[lastActual] ?? null }))\n      .filter((s): s is { label: string; value: number } => s.value != null)\n      .sort((a, b) => b.value - a.value);\n    const top = ranked[0];\n    if (!top) return null;\n    const share = shareOfTotal(top.value, current);\n    return share == null ? null : { label: top.label, share };\n  }, [streams, current, lastActual]);\n\n  return (\n    <div className={cn(\"space-y-4\", className)}>\n      <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-4\">\n        <Tile\n          definition={`${basis}. The total of the column, not a separate figure.`}\n          label={`Revenue · ${periods[lastActual]}`}\n        >\n          <Money size=\"md\" value={current == null ? null : { amount: current, currency }} />\n        </Tile>\n\n        <Tile\n          definition={\n            mom.relative == null\n              ? \"No percentage — the prior period was zero, negative or unposted.\"\n              : `Against ${periods[lastActual - 1] ?? \"the prior period\"}.`\n          }\n          label=\"On prior period\"\n        >\n          <span className=\"flex items-baseline gap-2\">\n            <Percent\n              size=\"md\"\n              value={mom.relative == null ? null : { kind: \"relative\", value: mom.relative }}\n            />\n            {mom.absolute != null ? (\n              <Money accounting size=\"xs\" tone=\"secondary\" value={{ amount: mom.absolute, currency }} />\n            ) : null}\n          </span>\n        </Tile>\n\n        <Tile\n          definition={\n            priorYear\n              ? \"The same period twelve months back — the read that survives seasonality.\"\n              : \"No prior year supplied. A year-on-year figure is not inferred from a shorter history.\"\n          }\n          label=\"On prior year\"\n        >\n          <Percent\n            size=\"md\"\n            value={yoy.relative == null ? null : { kind: \"relative\", value: yoy.relative }}\n          />\n        </Tile>\n\n        <Tile\n          definition=\"One posted period multiplied by twelve. A projection, not a booking, and not ARR — it takes no view of churn, seasonality or contract length.\"\n          label=\"Annualised run-rate\"\n        >\n          <Money\n            size=\"md\"\n            tone=\"secondary\"\n            value={projected == null ? null : { amount: projected, currency }}\n          />\n        </Tile>\n      </div>\n\n      <PeriodGrid\n        actualThrough={actualThrough}\n        caption={`Revenue by stream, ${periods[0]} to ${periods[periods.length - 1]}`}\n        columns={columns}\n        currency={currency}\n        footnote={\n          <span className=\"flex flex-wrap items-center gap-x-4 gap-y-1\">\n            <span>\n              {basis} · {periods.length} periods · rows marked ƒ are computed here, not read from the ledger.\n            </span>\n            {cagr != null ? (\n              <span>\n                Compound growth across the posted columns:{\" \"}\n                <span className=\"numeric font-medium text-text-secondary\">{cagr.toFixed(1)}%</span> per\n                period.\n              </span>\n            ) : null}\n          </span>\n        }\n        labelHeader=\"Stream\"\n        labelWidth={240}\n        rows={rows}\n        shareBase=\"__total\"\n        storageKey={storageKey}\n      />\n\n      {concentration && concentration.share >= 30 ? (\n        <p className=\"flex items-start gap-2 rounded-sm bg-warning-subtle px-3 py-2 text-body\">\n          <span aria-hidden=\"true\">▲</span>\n          <span>\n            <span className=\"font-medium\">{concentration.label}</span> is{\" \"}\n            <span className=\"numeric\">{concentration.share.toFixed(1)}%</span> of revenue in{\" \"}\n            {periods[lastActual]}. Concentration above a third is a fact about the risk in the business, not a\n            fact about the month — it belongs in the commentary either way.\n          </span>\n        </p>\n      ) : null}\n    </div>\n  );\n}\n\nfunction Tile({\n  label,\n  definition,\n  children,\n}: {\n  label: string;\n  definition: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"rounded-md border border-border bg-surface p-4\">\n      <span className=\"flex items-center gap-1 text-label font-medium text-text-secondary\">\n        {label}\n        <InfoTip about={label}>{definition}</InfoTip>\n      </span>\n      <span className=\"mt-1 block\">{children}</span>\n    </div>\n  );\n}\n\n/* Re-exported so a caller can build rows for a bespoke grid without reaching\n   past this module into the grid's own types. */\nexport type { GridUnit } from \"@/components/ledger/period-grid\";\n",
      "path": "components/ledger/revenue-tracker.tsx",
      "target": "components/ledger/revenue-tracker.tsx",
      "type": "registry:component"
    }
  ],
  "name": "revenue-tracker",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/info-tip",
    "@lopifi/money",
    "@lopifi/percent",
    "@lopifi/period-change",
    "@lopifi/period-grid"
  ],
  "title": "RevenueTracker",
  "type": "registry:component"
}
