{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "currency-input",
  "title": "Currency Input",
  "description": "A money input that shows a grouped, currency-formatted amount ($1,234.50) when idle and the raw number while you're editing, so the cursor never fights the thousands separators or symbol. Use it in any form that takes an amount: a price or product cost field, an invoice/quote line item, a budget/limit/goal, a donation, tip, or payment amount, a salary or rate field, an expense entry, or a checkout total. Common asks it answers: \"currency input\", \"money input\", \"price field\", \"formatted amount input\", \"thousands separator input\", \"dollar/euro input\", \"react-currency-input alternative\". shadcn/ui ships no currency or money field — you'd otherwise bolt masking onto its Input by hand; this packages it: value/onValueChange work in plain numbers (major units, e.g. 1234.5), not strings, so there's no parsing on your side, and it commits the rounded number on blur to the currency's own precision. The symbol, grouping, symbol placement, and decimal places come from Intl.NumberFormat via the `currency` (ISO 4217, default USD) and `locale` (default en-US) props, so $/€/¥ and 2-decimal vs 0-decimal (JPY) currencies all render correctly with no hardcoding. Typing follows that same locale, so comma-decimal locales (de-DE, fr-FR) accept \"1.234,50\" and \"1234,50\" rather than silently misreading them, and grouping characters or a pasted currency symbol are ignored; set allowNegative for refunds or adjustments. It stays controlled or uncontrolled like a native input, forwards a ref to the real <input> (so <Label htmlFor>, name, placeholder, and form libraries like react-hook-form all work), uses inputMode=\"decimal\" for a numeric mobile keypad, and is styled with shadcn tokens (border-input, ring, muted-foreground, tabular-nums) for automatic light/dark theming with no extra dependencies beyond your cn util. Distinct from number-input, which is a stepper for counts/quantities with +/− buttons; this one is for formatted money.",
  "files": [
    {
      "path": "registry/ui/currency-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface CurrencyInputProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<\"input\">,\n    \"value\" | \"defaultValue\" | \"onChange\" | \"type\"\n  > {\n  /** Controlled amount in major units (1234.5 → \"$1,234.50\"). `null` clears it. */\n  value?: number | null\n  /** Initial amount for uncontrolled use. */\n  defaultValue?: number | null\n  /** Fires with the parsed number as the user types and on blur; `null` when empty. */\n  onValueChange?: (value: number | null) => void\n  /** ISO 4217 currency code driving the symbol and decimal places (default \"USD\"). */\n  currency?: string\n  /** BCP 47 locale driving grouping and symbol placement (default \"en-US\"). */\n  locale?: string\n  /** Allow negative amounts (default false). */\n  allowNegative?: boolean\n}\n\nexport const CurrencyInput = React.forwardRef<\n  HTMLInputElement,\n  CurrencyInputProps\n>(function CurrencyInput(\n  {\n    className,\n    value,\n    defaultValue,\n    onValueChange,\n    currency = \"USD\",\n    locale = \"en-US\",\n    allowNegative = false,\n    disabled,\n    onFocus,\n    onBlur,\n    ...props\n  },\n  forwardedRef\n) {\n  const innerRef = React.useRef<HTMLInputElement>(null)\n  React.useImperativeHandle(\n    forwardedRef,\n    () => innerRef.current as HTMLInputElement\n  )\n\n  const formatter = React.useMemo(\n    () => new Intl.NumberFormat(locale, { style: \"currency\", currency }),\n    [locale, currency]\n  )\n  // Decimals this currency uses (USD → 2, JPY → 0), used to round on blur.\n  const fractionDigits = React.useMemo(\n    () => formatter.resolvedOptions().maximumFractionDigits ?? 2,\n    [formatter]\n  )\n\n  // This locale's own group and decimal characters (de-DE → \".\" and \",\"), so the\n  // field can be typed in the same notation it displays back. Without them a\n  // comma locale mis-reads its own output: \"1.234,50\" parsed as 1.2345.\n  const separators = React.useMemo(() => {\n    const parts = new Intl.NumberFormat(locale).formatToParts(11111.1)\n    return {\n      group: parts.find((p) => p.type === \"group\")?.value ?? \",\",\n      decimal: parts.find((p) => p.type === \"decimal\")?.value ?? \".\",\n    }\n  }, [locale])\n\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState<number | null>(\n    () => defaultValue ?? null\n  )\n  const numericValue = isControlled ? value ?? null : internal\n\n  const [focused, setFocused] = React.useState(false)\n  const [editing, setEditing] = React.useState(\"\")\n\n  // Keep only digits, a single decimal separator, and an optional leading minus\n  // so the raw editing string is always a parseable number-in-progress. Group\n  // separators — and anything else pasted in, like a currency symbol — are\n  // dropped. A plain \".\" also counts as the decimal unless this locale groups\n  // with it, which keeps numeric keypads usable where that is unambiguous.\n  function sanitize(raw: string) {\n    let out = \"\"\n    let seenDecimal = false\n    for (const ch of raw) {\n      if (ch >= \"0\" && ch <= \"9\") out += ch\n      else if (ch === separators.group) continue\n      else if (\n        !seenDecimal &&\n        (ch === separators.decimal || (ch === \".\" && separators.group !== \".\"))\n      ) {\n        out += separators.decimal\n        seenDecimal = true\n      } else if (ch === \"-\" && allowNegative && out === \"\") out += \"-\"\n    }\n    return out\n  }\n\n  // \"\" / \"-\" / a lone separator are in-progress, not a number yet → null.\n  function parse(str: string): number | null {\n    const normalized = str.split(separators.decimal).join(\".\")\n    if (\n      normalized === \"\" ||\n      normalized === \"-\" ||\n      normalized === \".\" ||\n      normalized === \"-.\"\n    )\n      return null\n    const n = Number(normalized)\n    return Number.isFinite(n) ? n : null\n  }\n\n  // Round to the currency's precision and drop float noise (0.1+0.2) before it\n  // seeds the raw editing string, in this locale's decimal notation.\n  function toEditString(n: number) {\n    const factor = 10 ** fractionDigits\n    return String(Math.round(n * factor) / factor).replace(\".\", separators.decimal)\n  }\n\n  // Show the grouped, symbol-prefixed amount when idle and the raw number while\n  // editing, so the cursor never fights the separators.\n  const display = focused\n    ? editing\n    : numericValue === null\n      ? \"\"\n      : formatter.format(numericValue)\n\n  function handleFocus(e: React.FocusEvent<HTMLInputElement>) {\n    setFocused(true)\n    setEditing(numericValue === null ? \"\" : toEditString(numericValue))\n    onFocus?.(e)\n    // Select the whole amount so the next keystroke replaces it, as money\n    // fields usually do.\n    requestAnimationFrame(() => innerRef.current?.select())\n  }\n\n  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {\n    const next = sanitize(e.target.value)\n    setEditing(next)\n    const parsed = parse(next)\n    if (!isControlled) setInternal(parsed)\n    onValueChange?.(parsed)\n  }\n\n  function handleBlur(e: React.FocusEvent<HTMLInputElement>) {\n    setFocused(false)\n    const typed = parse(editing)\n    let committed = typed\n    if (committed !== null) {\n      const factor = 10 ** fractionDigits\n      committed = Math.round(committed * factor) / factor\n    }\n    if (!isControlled) setInternal(committed)\n    // Re-emit only when rounding actually changed the value the parent last saw.\n    if (committed !== typed) onValueChange?.(committed)\n    onBlur?.(e)\n  }\n\n  return (\n    <input\n      ref={innerRef}\n      type=\"text\"\n      inputMode=\"decimal\"\n      autoComplete=\"off\"\n      disabled={disabled}\n      value={display}\n      onChange={handleChange}\n      onFocus={handleFocus}\n      onBlur={handleBlur}\n      className={cn(\n        \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm tabular-nums shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    />\n  )\n})\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}