{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "dashboard-charts",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Daily heatmap of cash movement.",
  "files": [
    {
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatDate } from \"@/lib/ledger/fiscal-period\";\nimport { formatMoney } from \"@/lib/ledger/format-money\";\n\nexport type CashDay = { date: Date; inflow: number; outflow: number };\n\nexport interface CashCalendarProps {\n  className?: string;\n  currency: string;\n  days: CashDay[];\n}\n\n/**\n * Two separate sequential ramps that differ in lightness, not a red/green\n * diverging scale — a diverging scale has no redundant sign cue available.\n */\nexport function CashCalendar({ days, currency, className }: CashCalendarProps) {\n  const maxIn = Math.max(...days.map((d) => d.inflow), 1);\n  const maxOut = Math.max(...days.map((d) => d.outflow), 1);\n\n  return (\n    <div className={cn(\"w-full\", className)}>\n      {/* A list of days, not an ARIA grid: a grid owes its cells rows and\n          arrow-key navigation, and a heat strip has neither. The weekday\n          initials are visual furniture; every day speaks its own date. */}\n      <ul aria-label=\"Daily cash movement\" className=\"grid grid-cols-7 gap-1\">\n        {[\"M\", \"T\", \"W\", \"T\", \"F\", \"S\", \"S\"].map((d, i) => (\n          <li aria-hidden=\"true\" className=\"pb-1 text-center text-caption text-text-tertiary\" key={i}>\n            {d}\n          </li>\n        ))}\n        {days.map((day) => {\n          const net = day.inflow - day.outflow;\n          const intensity = net >= 0 ? Math.min(1, day.inflow / maxIn) : Math.min(1, day.outflow / maxOut);\n          // Sequential lightness within one hue; the two ramps differ in hue\n          // *and* in lightness band, so they separate in greyscale too.\n          const lightness = net >= 0 ? 0.95 - intensity * 0.4 : 0.97 - intensity * 0.25;\n          const hue = net >= 0 ? 258 : 75;\n          const chroma = net >= 0 ? 0.02 + intensity * 0.1 : 0.02 + intensity * 0.12;\n\n          return (\n            <li\n              className=\"aspect-square rounded-xs border border-border-subtle\"\n              key={day.date.toISOString()}\n              style={{ background: `oklch(${lightness} ${chroma} ${hue})` }}\n              title={`${formatDate(day.date)} · in ${formatMoney({ amount: day.inflow, currency })} · out ${formatMoney({ amount: day.outflow, currency })}`}\n            >\n              <span className=\"sr-only\">\n                {`${formatDate(day.date)}, net ${formatMoney({ amount: net, currency }, { accounting: true })}`}\n              </span>\n            </li>\n          );\n        })}\n      </ul>\n      <div className=\"mt-3 flex items-center gap-4 text-caption text-text-secondary\">\n        <Legend hue={258} label=\"Net inflow\" />\n        <Legend hue={75} label=\"Net outflow\" />\n      </div>\n    </div>\n  );\n}\n\nfunction Legend({ hue, label }: { hue: number; label: string }) {\n  return (\n    <span className=\"flex items-center gap-1.5\">\n      <span aria-hidden=\"true\" className=\"flex\">\n        {[0.95, 0.82, 0.68, 0.55].map((l) => (\n          <span\n            className=\"size-3 border border-border-subtle\"\n            key={l}\n            style={{ background: `oklch(${l} ${0.02 + (0.95 - l) * 0.25} ${hue})` }}\n          />\n        ))}\n      </span>\n      {label}\n    </span>\n  );\n}\n",
      "path": "components/ledger/cash-calendar.tsx",
      "target": "components/ledger/cash-calendar.tsx",
      "type": "registry:component"
    }
  ],
  "name": "cash-calendar",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/fiscal-period",
    "@lopifi/format-money"
  ],
  "title": "CashCalendar",
  "type": "registry:component"
}
