{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "cards",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "The hero: one trusted number.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useValueFlash } from \"@/hooks/ledger/use-live-value\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { isNegative } from \"@/lib/ledger/format-money\";\nimport type { MoneyValue, PercentChange, Polarity } from \"@/lib/ledger/money.types\";\nimport { AsOf } from \"@/components/ledger/as-of\";\nimport { Card } from \"@/components/ledger/card\";\nimport { Delta } from \"@/components/ledger/delta\";\nimport { EntityMark } from \"@/components/ledger/entity-mark\";\nimport { Figure } from \"@/components/ledger/figure\";\nimport { Sparkline } from \"@/components/ledger/sparkline\";\nimport { StaleIndicator } from \"@/components/ledger/stale-indicator\";\nimport { StatusPill } from \"@/components/ledger/status-pill\";\n\nexport interface BalanceCardProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  actions?: React.ReactNode;\n  asOf?: Date;\n  /**\n   * A bank balance has two values and they differ. `available` is the figure;\n   * `current` is a secondary line, shown only when they diverge. Conflating\n   * them is the most consequential copy error in consumer banking UI.\n   */\n  available: MoneyValue | null;\n  basis?: string;\n  current?: MoneyValue | null;\n  data?: number[];\n  delta?: MoneyValue | null;\n  eyebrow: string;\n  institution?: string;\n  institutionSrc?: string;\n  live?: boolean;\n  percent?: PercentChange;\n  polarity?: Polarity;\n  redacted?: boolean;\n  stale?: { lastSynced: Date; onRetry?: () => void };\n  timeZone?: string;\n}\n\n/**\n * The hero. One trusted number — the component a user opens the product\n * to see.\n *\n * 3 sizes, 2 weights: eyebrow (label/500), figure (hero/500), delta\n * (caption/400). The as-of and any secondary line reuse caption/400, which\n * is a fourth slot at the same rank rather than a fourth rank.\n */\nexport const BalanceCard = React.forwardRef<HTMLDivElement, BalanceCardProps>(\n  (\n    {\n      className,\n      eyebrow,\n      available,\n      current,\n      delta,\n      percent,\n      polarity = \"standard\",\n      basis,\n      asOf,\n      timeZone,\n      data,\n      institution,\n      institutionSrc,\n      redacted = false,\n      stale,\n      live = false,\n      actions,\n      ...props\n    },\n    ref,\n  ) => {\n    const amount = available\n      ? typeof available.amount === \"bigint\"\n        ? Number(available.amount)\n        : available.amount\n      : null;\n    const { flash, key } = useValueFlash(live ? amount : undefined);\n    const overdrawn = available != null && isNegative(available);\n\n    const diverges =\n      current != null && available != null && Number(current.amount) !== Number(available.amount);\n\n    return (\n      <Card\n        className={cn(\"min-w-[280px] overflow-hidden\", className)}\n        ref={ref}\n        stale={Boolean(stale)}\n        {...props}\n      >\n        {/* The eyebrow row carries the account's *state*. An overdrawn pill is a\n            fact about the account, not about the change, and putting it down in\n            the delta row was what made that row read as a run-on sentence. */}\n        <div className=\"flex items-start justify-between gap-3\">\n          <span className=\"flex min-w-0 items-center gap-2\">\n            <span className=\"truncate text-label font-medium text-text-secondary\">{eyebrow}</span>\n            {overdrawn ? <StatusPill entity=\"Account\" status=\"overdue\" /> : null}\n          </span>\n          {institution ? (\n            <EntityMark category=\"bank\" name={institution} size={24} src={institutionSrc} />\n          ) : null}\n        </div>\n\n        <div\n          aria-live={live ? \"polite\" : undefined}\n          className={cn(\n            \"mt-2 -mx-1 rounded-xs px-1\",\n            flash === \"positive\" && \"ledger-flash-positive\",\n            flash === \"negative\" && \"ledger-flash-negative\",\n          )}\n          key={key}\n        >\n          <Figure colorBySign={overdrawn} redacted={redacted} size=\"hero\" value={available} />\n        </div>\n\n        {diverges ? (\n          <p className=\"mt-1 text-caption text-text-secondary\">\n            Current <span className=\"numeric\">{formatSecondary(current)}</span>\n          </p>\n        ) : null}\n\n        {/* Change on the left, freshness on the right — two facts at opposite\n            ends rather than four clauses in a row. The comparison basis rides\n            with the delta as a tooltip, because it is a restatement of a date\n            the delta already implies, not a fact the reader has to be told. */}\n        <div className=\"mt-2 flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1\">\n          {delta !== undefined ? (\n            <Delta basisTooltip={basis} percent={percent} polarity={polarity} size=\"xs\" value={delta} />\n          ) : (\n            <span />\n          )}\n          {asOf ? <AsOf className=\"shrink-0\" compact date={asOf} timeZone={timeZone} /> : null}\n        </div>\n\n        {stale ? (\n          <div className=\"mt-3\">\n            <StaleIndicator lastSynced={stale.lastSynced} onRetry={stale.onRetry} />\n          </div>\n        ) : null}\n\n        {actions ? <div className=\"mt-4 flex gap-2\">{actions}</div> : null}\n\n        {data ? (\n          // Full-bleed to three edges.\n          <div className=\"-mx-6 -mb-6 mt-4 aspect-[4/1] overflow-hidden\">\n            <Sparkline className=\"h-full\" data={data} height={64} polarity={polarity} showEndpoint={false} />\n          </div>\n        ) : null}\n      </Card>\n    );\n  },\n);\nBalanceCard.displayName = \"BalanceCard\";\n\nfunction formatSecondary(value: MoneyValue | null | undefined) {\n  if (!value) return \"n/a\";\n  return new Intl.NumberFormat(\"en-US\", {\n    currency: value.currency,\n    currencyDisplay: \"narrowSymbol\",\n    currencySign: \"accounting\",\n    style: \"currency\",\n  }).format(Number(value.amount) / 10 ** (value.scale ?? 2));\n}\n",
      "path": "components/ledger/balance-card.tsx",
      "target": "components/ledger/balance-card.tsx",
      "type": "registry:component"
    }
  ],
  "name": "balance-card",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/as-of",
    "@lopifi/card",
    "@lopifi/cn",
    "@lopifi/delta",
    "@lopifi/entity-mark",
    "@lopifi/figure",
    "@lopifi/format-money",
    "@lopifi/money.types",
    "@lopifi/sparkline",
    "@lopifi/stale-indicator",
    "@lopifi/status-pill",
    "@lopifi/use-live-value"
  ],
  "title": "BalanceCard",
  "type": "registry:component"
}
