{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "price",
  "type": "registry:ui",
  "title": "Price",
  "description": "One amount at the title weight with tabular figures, formatted from Shopify's MoneyV2 shape (an amount and a currency code) in the page's locale. A sale strikes the old price through with hidden words, and can add a neutral tag with the saving. Never red, never the accent.",
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/tag.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/price.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/price 2026.10.24. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Price: one amount, with what it was and what it costs per unit when those matter (components.md, \"Price\").\n *\n * - Money is an amount and a currency code, Shopify's MoneyV2 shape ({ amount: \"90.0\", currencyCode: \"EUR\" }), so a\n *   Storefront API price needs no mapping. The amount may be a string or a number.\n * - Type: the amount at the size of where it sits, the title weight, --litt-fg, in the sans with tabular figures, as a\n *   Stat's value. Formatted by Intl.NumberFormat in the page's locale (or `locale`). Whole amounts drop the cents\n *   (\"€90\"), any other keeps two places (\"€12.50\"), so a price never reads \"€90.00\" beside \"€12.50\" for no reason.\n * - From: a product whose variants differ in price shows its lowest after \"From\" in --litt-fg-subtle at the text weight.\n * - Sale: a compare-at price higher than the price follows it, struck through, in --litt-fg-subtle at the text weight.\n *   Hidden words say \"Sale price\" and \"Regular price\", so the strike is never the only signal. `saving` adds a neutral\n *   Tag after it (\"Save 25%\"). Never red, never the accent, never a status color: a sale is not a status.\n * - Unit price: an optional line under it at the caption size in --litt-fg-subtle (\"€4.20 / 100 ml\").\n * - Motion: none. A price that changes just changes; a total that changes while someone watches is a Rolling number.\n */\nimport { Tag } from \"./tag\";\n\n/** An amount and its currency, as Shopify's Storefront API gives it: { amount: \"90.0\", currencyCode: \"EUR\" }. */\nexport type Money = {\n  /** The amount, as a decimal string or a number. */\n  amount: string | number;\n  /** The ISO 4217 currency code: EUR, USD, GBP. */\n  currencyCode: string;\n};\n\nconst amountOf = (money: Money) => (typeof money.amount === \"number\" ? money.amount : Number.parseFloat(money.amount));\n\n/** Formats money in the page's locale (or the one given): \"€90\" for a whole amount, \"€12.50\" for any other. */\nexport function formatMoney(money: Money, locale?: string): string {\n  const n = amountOf(money);\n  const whole = Number.isInteger(n);\n  return new Intl.NumberFormat(locale, {\n    style: \"currency\",\n    currency: money.currencyCode,\n    minimumFractionDigits: whole ? 0 : 2,\n    maximumFractionDigits: whole ? 0 : 2,\n  }).format(n);\n}\n\n/** The same money times a count, such as a line's unit price times its quantity. */\nexport const multiplyMoney = (money: Money, by: number): Money => ({\n  amount: Math.round(amountOf(money) * by * 100) / 100,\n  currencyCode: money.currencyCode,\n});\n\n/** The sum of several amounts in one currency, such as a cart's subtotal. An empty list gives zero in `currencyCode`. */\nexport const sumMoney = (list: Money[], currencyCode = list[0]?.currencyCode ?? \"EUR\"): Money => ({\n  amount: Math.round(list.reduce((sum, m) => sum + amountOf(m) * 100, 0)) / 100,\n  currencyCode,\n});\n\n/** How much a compare-at price saves, as a whole percent, or null when it saves nothing. */\nexport function savingOf(price: Money, compareAt?: Money | null): number | null {\n  if (!compareAt) return null;\n  const now = amountOf(price);\n  const was = amountOf(compareAt);\n  if (!(was > now) || was <= 0) return null;\n  return Math.round(((was - now) / was) * 100);\n}\n\nexport type PriceProps = {\n  /** What it costs now. */\n  price: Money;\n  /** What it cost before a sale. Shown struck through after the price, only when it is higher. */\n  compareAt?: Money | null;\n  /** Says \"From\" before the price, for a product whose variants differ in price. Pass its lowest. */\n  from?: boolean;\n  /** Adds a neutral tag with what the sale saves (\"Save 25%\"). Use it on a product's page, not in a grid. */\n  saving?: boolean;\n  /** What it costs per unit, on a line under the price: { price, per: \"100 ml\" }. */\n  unitPrice?: { price: Money; per: string };\n  /** The locale to format in. Leave it out for the page's. */\n  locale?: string;\n  /** Extra classes for the price, such as a size: it takes the size of where it sits. */\n  className?: string;\n};\n\n/** One amount at the title weight, with a struck-through compare-at price and a unit price when they apply. */\nexport function Price({ price, compareAt, from = false, saving = false, unitPrice, locale, className = \"\" }: PriceProps) {\n  const saved = savingOf(price, compareAt);\n  const sale = saved !== null;\n  const line = (\n    <span className=\"inline-flex flex-wrap items-baseline gap-x-2 gap-y-1\">\n      {from && <span className=\"text-litt-fg-subtle\">From</span>}\n      <span className=\"font-litt-title text-litt-fg tabular-nums\">\n        {sale && <span className=\"sr-only\">Sale price </span>}\n        {formatMoney(price, locale)}\n      </span>\n      {sale && compareAt && (\n        <s className=\"text-litt-fg-subtle tabular-nums\">\n          <span className=\"sr-only\">Regular price </span>\n          {formatMoney(compareAt, locale)}\n        </s>\n      )}\n      {sale && saving && <Tag className=\"self-center\">{`Save ${saved}%`}</Tag>}\n    </span>\n  );\n  if (!unitPrice) return <span className={\"inline-block \" + className}>{line}</span>;\n  return (\n    <span className={\"inline-grid gap-1 \" + className}>\n      {line}\n      <span className=\"text-litt-caption font-normal text-litt-fg-subtle tabular-nums\">\n        {formatMoney(unitPrice.price, locale)} / {unitPrice.per}\n      </span>\n    </span>\n  );\n}\n",
      "target": "components/litt/price.tsx"
    }
  ],
  "docs": "Import Price and formatMoney from components/litt/price. Pass price as { amount, currencyCode }; compareAt shows only when it is higher. multiplyMoney and sumMoney do a line's and a cart's sums.",
  "meta": {
    "version": "2026.10.24",
    "released": "2026-10-24",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
