import NumberFlow from "@number-flow/react"; import { useMoney } from "@/hooks/useMoney"; import { formatMoney, localeFor, maskedMoney } from "@/lib/format"; import { cn } from "@/lib/utils"; /** * A headline amount that counts to its new value when it changes (a refresh, * switching month). Uses the user's currency setting by default; in privacy * mode it shows the mask instead. NumberFlow honours prefers-reduced-motion * on its own. * * Pass `currency` to override the app-wide setting for amounts that carry * their own currency regardless of it, e.g. an investment holding priced in * its native market's currency. */ export function AnimatedMoney({ value, currency: currencyOverride, signDisplay = "auto", className, }: { value: number; currency?: string; signDisplay?: "auto" | "exceptZero" | "never"; className?: string; }) { const { currency: appCurrency, hidden } = useMoney(); const currency = currencyOverride ?? appCurrency; if (hidden) { return ( {maskedMoney(currency, Number.isFinite(value) ? value : 0, { signDisplay })} ); } const n = Number.isFinite(value) ? value : 0; const format = { style: "currency", currency, minimumFractionDigits: 2, maximumFractionDigits: 2, signDisplay } as const; let label: string; try { label = new Intl.NumberFormat(localeFor(currency), format).format(n); } catch { // A currency setting Intl doesn't recognise: show the plain formatter's // fallback text rather than letting NumberFlow throw mid-render. return {formatMoney(n, currency, { signDisplay })}; } return ( {/* NumberFlow renders spinning digit strips in its shadow DOM, which read as noise to assistive tech; they get a plain-text twin instead. */} {label} ); }