{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "statements",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Period-over-period explanations.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatTimestamp } from \"@/lib/ledger/fiscal-period\";\nimport type { MoneyValue, Polarity } from \"@/lib/ledger/money.types\";\nimport { Button } from \"@/components/ledger/button\";\nimport { Delta } from \"@/components/ledger/delta\";\nimport { ChatBubbleLeftIcon, CheckIcon, ExclamationTriangleIcon } from \"@/components/ledger/icons\";\nimport { Money } from \"@/components/ledger/money\";\n\n/** \"Revenue up 12%\" is not an explanation; \"volume up 18%, price down 5%\" is. */\nexport const FLUX_DRIVERS = [\n  \"Volume\",\n  \"Price\",\n  \"Mix\",\n  \"Timing\",\n  \"One-time item\",\n  \"FX\",\n  \"Reclassification\",\n  \"Error correction\",\n] as const;\n\nexport type FluxItem = {\n  id: string;\n  line: string;\n  current: MoneyValue;\n  prior: MoneyValue;\n  variance: MoneyValue;\n  percent: number | null;\n  polarity: Polarity;\n  material: boolean;\n  explanation?: { driver: string; text: string; author: string; at: Date };\n};\n\nexport interface FluxAnalysisProps {\n  className?: string;\n  items: FluxItem[];\n  /** Displayed, so a reviewer knows what they are *not* seeing. */\n  materiality: { absolute: MoneyValue; relative: number };\n  onExplain?: (id: string, driver: string, text: string) => void;\n}\n\n/**\n * The actual month-end close workflow, and no library supports it.\n * Unexplained material variances are the reviewer's worklist.\n */\nexport function FluxAnalysis({ items, materiality, onExplain, className }: FluxAnalysisProps) {\n  const [drafting, setDrafting] = React.useState<string | null>(null);\n  const [driver, setDriver] = React.useState<string>(FLUX_DRIVERS[0]);\n  const [text, setText] = React.useState(\"\");\n\n  const unexplained = items.filter((i) => i.material && !i.explanation);\n\n  return (\n    <div className={cn(\"space-y-4\", className)}>\n      <div className=\"flex flex-wrap items-center gap-3 rounded-md border border-border bg-surface px-4 py-3\">\n        <span className=\"text-label font-medium\">Flux analysis</span>\n        <span className=\"text-caption text-text-secondary\">\n          Flagging variances over <Money size=\"xs\" tone=\"inherit\" value={materiality.absolute} /> and{\" \"}\n          <span className=\"numeric\">{materiality.relative}%</span> — both must be exceeded.\n        </span>\n        <span\n          className={cn(\n            \"ml-auto flex items-center gap-1.5 rounded-sm px-2 py-1 text-caption font-medium\",\n            unexplained.length === 0 ? \"bg-positive-subtle text-positive\" : \"bg-warning-subtle text-warning\",\n          )}\n        >\n          {unexplained.length === 0 ? (\n            <CheckIcon aria-hidden=\"true\" className=\"size-3.5\" />\n          ) : (\n            <ExclamationTriangleIcon aria-hidden=\"true\" className=\"size-3.5\" />\n          )}\n          {unexplained.length === 0\n            ? \"All material variances explained\"\n            : `${unexplained.length} unexplained`}\n        </span>\n      </div>\n\n      <ul className=\"space-y-2\">\n        {items.map((item) => (\n          <li\n            className={cn(\n              \"rounded-md border bg-surface p-4\",\n              item.material && !item.explanation ? \"border-[var(--warning)]\" : \"border-border\",\n            )}\n            key={item.id}\n          >\n            <div className=\"flex flex-wrap items-baseline justify-between gap-3\">\n              <span className=\"text-body font-medium\">{item.line}</span>\n              <span className=\"flex flex-wrap items-baseline gap-x-3 gap-y-1\">\n                <Money accounting tone=\"secondary\" value={item.prior} />\n                <span aria-hidden=\"true\" className=\"text-text-tertiary\">\n                  →\n                </span>\n                <Money accounting value={item.current} />\n                <Delta\n                  percent={item.percent === null ? undefined : { kind: \"relative\", value: item.percent }}\n                  polarity={item.polarity}\n                  value={item.variance}\n                />\n              </span>\n            </div>\n\n            {item.explanation ? (\n              <div className=\"mt-3 rounded-sm bg-surface-sunken p-3\">\n                <p className=\"text-caption font-medium text-text-secondary\">{item.explanation.driver}</p>\n                <p className=\"mt-0.5 text-body\">{item.explanation.text}</p>\n                <p className=\"mt-1 text-caption text-text-tertiary\">\n                  {item.explanation.author} · {formatTimestamp(item.explanation.at)}\n                </p>\n              </div>\n            ) : item.material ? (\n              drafting === item.id ? (\n                <div className=\"mt-3 space-y-2\">\n                  <select\n                    aria-label=\"Variance driver\"\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) => setDriver(e.target.value)}\n                    value={driver}\n                  >\n                    {FLUX_DRIVERS.map((d) => (\n                      <option key={d}>{d}</option>\n                    ))}\n                  </select>\n                  <textarea\n                    aria-label=\"Explanation\"\n                    className=\"w-full rounded-sm border border-border bg-surface p-2 text-body outline-none focus-visible:border-accent\"\n                    onChange={(e) => setText(e.target.value)}\n                    placeholder=\"What drove it? Be specific — volume, price, mix.\"\n                    rows={2}\n                    value={text}\n                  />\n                  <div className=\"flex gap-2\">\n                    <Button\n                      disabled={!text.trim()}\n                      onClick={() => {\n                        onExplain?.(item.id, driver, text);\n                        setDrafting(null);\n                        setText(\"\");\n                      }}\n                      size=\"sm\"\n                      variant=\"primary\"\n                    >\n                      Save explanation\n                    </Button>\n                    <Button onClick={() => setDrafting(null)} size=\"sm\" variant=\"ghost\">\n                      Cancel\n                    </Button>\n                  </div>\n                </div>\n              ) : (\n                <Button className=\"mt-3\" onClick={() => setDrafting(item.id)} size=\"sm\" variant=\"secondary\">\n                  <ChatBubbleLeftIcon aria-hidden=\"true\" className=\"size-3.5\" />\n                  Explain this variance\n                </Button>\n              )\n            ) : null}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n",
      "path": "components/ledger/flux-analysis.tsx",
      "target": "components/ledger/flux-analysis.tsx",
      "type": "registry:component"
    }
  ],
  "name": "flux-analysis",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/button",
    "@lopifi/cn",
    "@lopifi/delta",
    "@lopifi/fiscal-period",
    "@lopifi/icons",
    "@lopifi/money",
    "@lopifi/money.types"
  ],
  "title": "FluxAnalysis",
  "type": "registry:component"
}
