{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "primitives",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Percent vs. percentage points, explicitly discriminated.",
  "files": [
    {
      "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatPercent } from \"@/lib/ledger/format-money\";\nimport type { PercentChange } from \"@/lib/ledger/money.types\";\n\nexport interface PercentProps extends Omit<React.ComponentPropsWithoutRef<\"span\">, \"children\"> {\n  signed?: boolean;\n  size?: \"xs\" | \"sm\" | \"md\";\n  /** No default kind. Conflating percent with percentage points is the bug. */\n  value: PercentChange | null;\n}\n\nconst SIZES = {\n  md: \"text-figure-md font-medium\",\n  sm: \"text-figure-sm\",\n  xs: \"text-figure-xs\",\n} as const;\n\n/**\n * A margin moving 20% → 25% rose 5 percentage points and 25% relative.\n * Both are correct; they are different numbers. The discriminator is required.\n *\n * Division by zero renders `n/a` — never Infinity, never NaN, never 100%.\n */\nexport const Percent = React.forwardRef<HTMLSpanElement, PercentProps>(\n  ({ className, value, signed = true, size = \"sm\", ...props }, ref) => {\n    if (value == null || !Number.isFinite(value.value)) {\n      return (\n        <span\n          className={cn(\"numeric relative text-text-tertiary\", SIZES[size], className)}\n          ref={ref}\n          {...props}\n        >\n          <span aria-hidden=\"true\">n/a</span>\n          <span className=\"sr-only select-none\">not available</span>\n        </span>\n      );\n    }\n\n    const text = formatPercent(value, { signed });\n    const spoken =\n      value.kind === \"points\"\n        ? `${text.replace(\"pp\", \" percentage points\")}`\n        : value.kind === \"bps\"\n          ? text.replace(\" bps\", \" basis points\")\n          : text.replace(\"%\", \" percent\");\n\n    return (\n      <span className={cn(\"numeric relative whitespace-nowrap\", SIZES[size], className)} ref={ref} {...props}>\n        <span aria-hidden=\"true\">{text}</span>\n        {/* Spoken text, not an aria-label: a label on a role-less span is\n            prohibited ARIA. select-none keeps it out of a copied selection. */}\n        <span className=\"sr-only select-none\">{spoken.replace(\"−\", \"negative \")}</span>\n      </span>\n    );\n  },\n);\nPercent.displayName = \"Percent\";\n",
      "path": "components/ledger/percent.tsx",
      "target": "components/ledger/percent.tsx",
      "type": "registry:ui"
    }
  ],
  "name": "percent",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/format-money",
    "@lopifi/money.types"
  ],
  "title": "Percent",
  "type": "registry:ui"
}
