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}
);
}