{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "metrics-analysis",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "The inputs a forecast is built from, and what they produce.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { InfoTip } from \"@/components/ledger/info-tip\";\nimport type { GridUnit, PeriodColumn, PeriodRow } from \"@/components/ledger/period-grid\";\nimport { PeriodGrid } from \"@/components/ledger/period-grid\";\nimport { Rule } from \"@/components/ledger/rule\";\n\nexport interface ModelDriver {\n  /** An output of the model rather than an input to it. Marked ƒ, never typed into. */\n  computed?: boolean;\n  /** How the figure is arrived at. Rendered under the label, always. */\n  formula?: string;\n  id: string;\n  label: string;\n  level?: number;\n  polarity?: \"standard\" | \"inverted\" | \"neutral\";\n  /** The unit decides the render. A headcount is not a dollar. */\n  unit?: GridUnit;\n  /**\n   * One entry per period, scaled by 100 like every figure in the grid —\n   * $412,000 is 41_200_000, 38 people is 3800, 12.4% is 1240.\n   */\n  values: (number | null)[];\n  variant?: \"line\" | \"subtotal\" | \"total\" | \"section\" | \"ratio\";\n}\n\nexport interface DriverModelProps {\n  /** The index of the last actual. Only periods after it are assumptions. */\n  actualThrough: number;\n  /** Named assumptions behind the projection. Stated, never implied. */\n  assumptions?: { label: string; value: string; rationale?: string }[];\n  className?: string;\n  currency: string;\n  drivers: ModelDriver[];\n  /**\n   * A committed edit to a projected input cell, in minor units. The model\n   * lives with the caller — everything downstream of this cell has to move in\n   * the same render, and only the caller knows what downstream means.\n   */\n  onDriverEdit?: (edit: { driverId: string; periodIndex: number; value: number }) => void;\n  periods: string[];\n  scenario?: { value: string; options: string[]; onChange: (s: string) => void };\n  storageKey?: string;\n}\n\n/**\n * The driver model: the inputs a forecast is built from, and the outputs they\n * produce, in one grid with the boundary between them visible.\n *\n * Three separations do all the work here, and a model missing any one of them\n * is a spreadsheet with a plausible number at the bottom:\n *\n * 1. **Actual from projection.** The dashed edge and the shading. Everything\n *    left of it happened; everything right of it is a belief.\n * 2. **Input from output.** An input is typed; an output is computed and\n *    carries the ƒ mark and its formula. A model where you cannot tell which\n *    is which is a model where nobody can find the assumption that moved the\n *    answer.\n * 3. **Assumption from arithmetic.** The named assumptions sit under the\n *    grid in words — \"new logos hold at 14 a month\" — because that is the\n *    sentence someone will disagree with, and they cannot disagree with a\n *    growth rate buried three rows into a projected column.\n *\n * Only projected inputs accept typing. An actual is what the ledger posted;\n * a model that lets you type over it is a model that will eventually be used\n * to explain a month that never happened.\n */\nexport function DriverModel({\n  periods,\n  actualThrough,\n  drivers,\n  currency,\n  scenario,\n  onDriverEdit,\n  assumptions = [],\n  storageKey,\n  className,\n}: DriverModelProps) {\n  const columns: PeriodColumn[] = React.useMemo(\n    () =>\n      periods.map((p, i) => ({\n        id: p,\n        kind: i > actualThrough ? \"forecast\" : \"actual\",\n        label: p,\n        locked: i === actualThrough,\n      })),\n    [periods, actualThrough],\n  );\n\n  const rows: PeriodRow[] = React.useMemo(\n    () =>\n      drivers.map((d) => ({\n        derived: d.computed,\n        id: d.id,\n        label: d.label,\n        level: d.level ?? 1,\n        note: d.formula,\n        polarity: d.polarity ?? \"standard\",\n        unit: d.unit ?? \"money\",\n        values: d.values,\n        variant: d.variant ?? (d.computed ? \"subtotal\" : \"line\"),\n      })),\n    [drivers],\n  );\n\n  const byId = React.useMemo(() => new Map(drivers.map((d) => [d.id, d])), [drivers]);\n  const periodIndex = React.useMemo(() => new Map(periods.map((p, i) => [p, i])), [periods]);\n\n  return (\n    <div className={cn(\"space-y-4\", className)}>\n      <PeriodGrid\n        actualThrough={actualThrough}\n        caption={`Driver model${scenario ? `, ${scenario.value} scenario` : \"\"}`}\n        columns={columns}\n        currency={currency}\n        /* Projected inputs only. Never an actual, never an output. */\n        editable={(row, column) => {\n          const driver = byId.get(row.id);\n          const index = periodIndex.get(column.id);\n          return driver != null && !driver.computed && index != null && index > actualThrough;\n        }}\n        footnote={\n          <>\n            Columns after <span className=\"font-medium\">{periods[actualThrough]}</span> are assumptions.{\" \"}\n            {onDriverEdit ? (\n              <>\n                Projected inputs can be typed into — double-click a cell, or press Enter on it. The cell\n                accepts what a spreadsheet accepts, including <span className=\"numeric\">=1200+340</span> and{\" \"}\n                <span className=\"numeric\">1.2k</span>. Actuals and ƒ rows cannot be edited.\n              </>\n            ) : null}\n          </>\n        }\n        labelHeader=\"Driver\"\n        labelWidth={260}\n        onCellEdit={\n          onDriverEdit\n            ? ({ rowId, columnIndex, value }) =>\n                onDriverEdit({ driverId: rowId, periodIndex: columnIndex, value })\n            : undefined\n        }\n        rows={rows}\n        storageKey={storageKey}\n        toolbar={\n          scenario ? (\n            <label className=\"flex items-center gap-2\">\n              <span className=\"text-label font-medium text-text-secondary\">Scenario</span>\n              <select\n                className=\"h-8 rounded-sm border border-border bg-surface px-2 text-label outline-none focus-visible:border-accent focus-visible:ring-2 focus-visible:ring-accent-subtle\"\n                onChange={(e) => scenario.onChange(e.target.value)}\n                value={scenario.value}\n              >\n                {scenario.options.map((s) => (\n                  <option key={s}>{s}</option>\n                ))}\n              </select>\n            </label>\n          ) : null\n        }\n      />\n\n      {assumptions.length > 0 ? (\n        <div className=\"rounded-xl border border-border bg-surface p-6\">\n          <h3 className=\"text-title-sm font-medium\">\n            Assumptions\n            <InfoTip about=\"Why assumptions are listed in words\">\n              A growth rate sitting in a projected column is arithmetic. The sentence it came from is the\n              thing a reader can actually disagree with, and disagreement is the whole point of a forecast\n              review.\n            </InfoTip>\n          </h3>\n          <Rule className=\"mt-3\" variant=\"subtotal\" />\n          <dl className=\"mt-3 space-y-3\">\n            {assumptions.map((a) => (\n              /* One group per assumption, its dt and dds direct children: a\n                 <dl> may hold only those (and the div that groups them). */\n              <div className=\"grid grid-cols-[1fr_auto] items-baseline gap-x-4\" key={a.label}>\n                <dt className=\"text-body\">{a.label}</dt>\n                <dd className=\"numeric text-body font-medium\">{a.value}</dd>\n                {a.rationale ? (\n                  <dd className=\"col-span-2 mt-0.5 text-caption text-text-tertiary\">{a.rationale}</dd>\n                ) : null}\n              </div>\n            ))}\n          </dl>\n        </div>\n      ) : null}\n    </div>\n  );\n}\n",
      "path": "components/ledger/driver-model.tsx",
      "target": "components/ledger/driver-model.tsx",
      "type": "registry:component"
    }
  ],
  "name": "driver-model",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/info-tip",
    "@lopifi/period-grid",
    "@lopifi/rule"
  ],
  "title": "DriverModel",
  "type": "registry:component"
}
