{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "primitives",
    "finance",
    "accounting"
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "description": "The core primitive. A monetary value, formatted correctly.",
  "files": [
    {
      "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatMoneyParts, isNegative, isZero, moneyAriaLabel, rawMoney } from \"@/lib/ledger/format-money\";\nimport type { MoneyValue } from \"@/lib/ledger/money.types\";\nimport { Tooltip } from \"@/components/ledger/tooltip\";\n\n/* Server-safe by design: a 400-row statement renders on the server. */\n\n/* `relative` anchors the absolutely-positioned spoken label to the figure, so\n   it can never widen a scroll container it sits in. */\nconst moneyVariants = cva(\"numeric relative inline-flex items-baseline whitespace-nowrap\", {\n  defaultVariants: { size: \"sm\", tone: \"primary\" },\n  variants: {\n    size: {\n      hero: \"text-figure-hero font-medium\",\n      lg: \"text-figure-lg font-medium\",\n      md: \"text-figure-md font-medium\",\n      sm: \"text-figure-sm font-normal\",\n      xs: \"text-figure-xs font-normal\",\n    },\n    tone: {\n      inherit: \"text-inherit\",\n      negative: \"text-negative\",\n      positive: \"text-positive\",\n      primary: \"text-text-primary\",\n      // Never --text-tertiary for a monetary value: money holds 7:1.\n      secondary: \"text-text-secondary\",\n    },\n  },\n});\n\nexport interface MoneyProps\n  extends Omit<React.ComponentPropsWithoutRef<\"span\">, \"children\">,\n    VariantProps<typeof moneyVariants> {\n  /** Parentheses for negatives — statements and tables. */\n  accounting?: boolean;\n  /** A converted value is a derived value: renders \"≈\" and states its basis. */\n  approximate?: { rate: number; asOf: Date; from: string };\n  /** Colour the figure by sign. Off by default; the glyphs carry the sign. */\n  colorBySign?: boolean;\n  /** Drop the cents. Display surfaces only — never a table or a statement. */\n  dropCents?: boolean;\n  locale?: string;\n  redacted?: boolean;\n  /** The ISO code — required in a mixed-currency context. */\n  showCode?: boolean;\n  /** Subordinate the symbol. Default true. */\n  showSymbol?: boolean;\n  /** Em dash for a true zero — statements only. */\n  statement?: boolean;\n  value: MoneyValue | null | undefined;\n}\n\n/**\n * The core primitive. Every monetary value in the system renders through it.\n *\n * Five slots, four optional: [≈] [symbol] [integer] [.decimals] [code].\n * The paren column is reserved on both sides regardless of sign, so a\n * right-aligned column of mixed-sign values keeps its decimal alignment.\n */\nexport const Money = React.forwardRef<HTMLSpanElement, MoneyProps>(\n  (\n    {\n      className,\n      value,\n      size,\n      tone,\n      accounting = false,\n      statement = false,\n      showSymbol = true,\n      showCode = false,\n      approximate,\n      redacted = false,\n      dropCents = false,\n      colorBySign = false,\n      locale,\n      ...props\n    },\n    ref,\n  ) => {\n    // Five distinct renders. Never `value ?? 0`.\n    if (value == null) {\n      return (\n        <span\n          className={cn(moneyVariants({ size, tone: \"secondary\" }), \"text-text-tertiary\", className)}\n          ref={ref}\n          {...props}\n        >\n          <span aria-hidden=\"true\">n/a</span>\n          <Spoken>not available</Spoken>\n        </span>\n      );\n    }\n\n    if (redacted) {\n      return (\n        <span\n          className={cn(moneyVariants({ size, tone }), \"text-text-tertiary\", className)}\n          ref={ref}\n          {...props}\n        >\n          <span aria-hidden=\"true\">••••</span>\n          <Spoken>hidden</Spoken>\n        </span>\n      );\n    }\n\n    if (statement && isZero(value)) {\n      return (\n        <span className={cn(moneyVariants({ size, tone }), className)} ref={ref} {...props}>\n          <span aria-hidden=\"true\">—</span>\n          <Spoken>zero</Spoken>\n          <ParenSlot />\n        </span>\n      );\n    }\n\n    const negative = isNegative(value);\n    const parts = formatMoneyParts(value, { dropCents, locale, showSymbol });\n    const useParens = accounting && negative;\n    const largeFigure = size === \"lg\" || size === \"hero\";\n    const resolvedTone =\n      colorBySign && negative ? \"negative\" : colorBySign && !isZero(value) ? \"positive\" : tone;\n\n    const label = moneyAriaLabel(value, { locale });\n    const spoken = approximate ? `approximately ${label}` : label;\n\n    const body = (\n      <span\n        className={cn(moneyVariants({ size, tone: resolvedTone }), className)}\n        data-raw={rawMoney(value, locale)}\n        ref={ref}\n        {...props}\n      >\n        {approximate ? (\n          // The figure face has no ≈; the text face does, at the same weight.\n          <span aria-hidden=\"true\" className=\"mr-1 font-sans text-text-tertiary\">\n            ≈\n          </span>\n        ) : null}\n\n        {/* Opening paren slot — reserved on both signs so digits never shift. */}\n        <span aria-hidden=\"true\" className={useParens ? undefined : \"invisible\"}>\n          (\n        </span>\n\n        {!useParens && negative ? (\n          // U+2212, not a hyphen: it aligns with the plus and sits on the\n          // lining figures once `case` is on.\n          <span aria-hidden=\"true\">−</span>\n        ) : null}\n\n        {parts.symbol ? (\n          <span aria-hidden=\"true\" className=\"mr-[var(--numeric-symbol-gap)] font-normal text-text-tertiary\">\n            {parts.symbol}\n          </span>\n        ) : null}\n\n        <span aria-hidden=\"true\">{parts.integer}</span>\n\n        {parts.fraction ? (\n          <span aria-hidden=\"true\" className={largeFigure ? \"text-text-tertiary\" : undefined}>\n            {parts.fraction}\n          </span>\n        ) : null}\n\n        {parts.symbolSuffix ? (\n          <span aria-hidden=\"true\" className=\"ml-1 font-normal text-text-tertiary\">\n            {parts.symbolSuffix}\n          </span>\n        ) : null}\n\n        <span aria-hidden=\"true\" className={useParens ? undefined : \"invisible\"}>\n          )\n        </span>\n\n        {showCode ? (\n          <span aria-hidden=\"true\" className=\"ml-1 text-caption text-text-tertiary\">\n            {value.currency}\n          </span>\n        ) : null}\n\n        <Spoken>{spoken}</Spoken>\n      </span>\n    );\n\n    // A converted value is a derived value. The basis — source currency, rate,\n    // and the instant it was struck — is the tooltip's whole job here.\n    if (!approximate) return body;\n    return <Tooltip content={approximateTitle(value, approximate, locale)}>{body}</Tooltip>;\n  },\n);\nMoney.displayName = \"Money\";\n\n/**\n * What a screen reader says in place of the glyphs, which are all\n * `aria-hidden`. Text rather than `aria-label`: a label on a role-less span is\n * prohibited ARIA and ignored by some readers. `select-none` keeps it out of a\n * copied selection, so a figure pasted into a spreadsheet is still the figure.\n */\nfunction Spoken({ children }: { children: React.ReactNode }) {\n  return <span className=\"sr-only select-none\">{children}</span>;\n}\n\n/** Keeps a `—` or `n/a` on the same decimal grid as its neighbours. */\nfunction ParenSlot() {\n  return (\n    <span aria-hidden=\"true\" className=\"invisible\">\n      )\n    </span>\n  );\n}\n\nfunction approximateTitle(\n  value: MoneyValue,\n  approximate: NonNullable<MoneyProps[\"approximate\"]>,\n  locale?: string,\n): string {\n  void value;\n  void locale;\n  const stamp = new Intl.DateTimeFormat(\"en-GB\", {\n    day: \"numeric\",\n    hour: \"2-digit\",\n    minute: \"2-digit\",\n    month: \"short\",\n    timeZone: \"UTC\",\n    year: \"numeric\",\n  }).format(approximate.asOf);\n  return `Converted from ${approximate.from} @ ${approximate.rate} · ${stamp} UTC`;\n}\n",
      "path": "components/ledger/money.tsx",
      "target": "components/ledger/money.tsx",
      "type": "registry:ui"
    }
  ],
  "name": "money",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/format-money",
    "@lopifi/money.types",
    "@lopifi/tooltip"
  ],
  "title": "Money",
  "type": "registry:ui"
}
