# CurrencyInput

> Money field whose value is an integer in minor units. The currency sets the decimals, Arabic and Western digits both parse, and the symbol sits where the locale puts it.

Source: https://docs.nasaqui.com/components/currency-input

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/currency-input.json
```

A field for an amount of money. The value you read and write is an **integer in minor units** (cents, halalas, fils):
`1999` is 19.99 USD, `12500` is 12.500 KWD, `1200` is 1,200 JPY. Nothing is a float, so nothing drifts. The currency
decides how many decimals the field accepts, and digits typed or pasted in any set (`1250.5`, `١٢٥٠٫٥`) give the same amount.

While the field has focus it shows the plain number you are editing. On blur it groups and pads it: `1,250.50`.

## When to use

- Prices, budgets, fees, invoice lines and any input the backend stores as minor units.
- Forms used in Arabic and English, where people paste amounts from spreadsheets and chats.
- An amount with a choosable currency (`currencies`).

## When not to use

- A plain quantity or percentage: use an `Input` with `inputMode="numeric"`.
- Showing a price: use [Price](https://docs.nasaqui.com/components/price) or `Num` with `style: "currency"` ([numeric](https://docs.nasaqui.com/components/numeric)).
- Converting between currencies at an exchange rate: this only keeps the same amount of money when the decimals differ.

## Import

```tsx
import { CurrencyInput, parseMoney, formatMinor } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { CurrencyInput } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Fee() {
  const [minor, setMinor] = useState<number | null>(1999);
  return <CurrencyInput aria-label="Fee" currency="USD" value={minor} onValueChange={setMinor} />;
}
```

## Anatomy

```
CurrencyInput                data-slot="currency-input" data-currency="USD"
└─ InputGroup                data-invalid when out of range
   ├─ symbol addon           data-slot="currency-symbol"  (start or end, where the locale puts it)
   ├─ input                  dir="ltr", inputMode="decimal" (numeric for 0 decimals)
   ├─ currency Select        only with `currencies`
   └─ hidden input           only with `name`: the minor-unit integer
```

## API

### `CurrencyInput`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value?` | `number \| null` | none | Controlled amount in minor units. `null` is empty. |
| `defaultValue?` | `number \| null` | `null` | Initial amount when uncontrolled. |
| `onValueChange?` | `(minor: number \| null) => void` | none | Fires on every edit with the amount in minor units, `null` when emptied. |
| `currency?` | `string` | `"USD"` | ISO 4217 code. Sets the decimals: JPY 0, USD 2, KWD 3. |
| `currencies?` | `readonly string[]` | none | Show a currency picker. Needs `onCurrencyChange`. |
| `onCurrencyChange?` | `(currency: string, minor: number \| null) => void` | none | The new code and the amount rewritten for its decimals (12.50 USD stays 12.500 in KWD, so `1250` becomes `12500`). |
| `min?` / `max?` | `number` | none | Inclusive bounds in minor units. Outside them the field is invalid. |
| `clampOnBlur?` | `boolean` | `false` | Pull an out-of-range amount back to the bound on blur. |
| `allowNegative?` | `boolean` | `false` | Accept a leading minus. |
| `numberingSystem?` | `"latn" \| "arab"` | `"latn"` | Digits shown. Both sets are accepted when typing. |
| `locale?` | `string` | provider locale | Decimal mark and symbol placement. |
| `overflow?` | `"round" \| "truncate"` | `"round"` | A pasted amount with too many decimals (`1.005` in USD). Typing simply stops at the decimals. |
| `symbol?` | `"symbol" \| "code" \| "none"` | `"symbol"` | `$`, `USD`, or nothing. |
| `fixedDecimals?` | `boolean` | `true` | Keep `.00` on whole amounts when not focused. |
| `step?` | `number` | one major unit | ArrowUp and ArrowDown change the amount by this many minor units; Shift multiplies by 10. |
| `name?` | `string` | none | Hidden input with the minor-unit integer, for native form posts. |
| `placeholder?` | `string` | `0.00` in the field's format | |
| `disabled?` / `readOnly?` / `invalid?` / `required?` | `boolean` | `false` | |
| `id?` / `aria-label?` | `string` | none | Give the input a name when there is no `FieldLabel`. |
| `labels?` | `CurrencyInputLabels` | Arabic and English | `currency` (picker name) and `outOfRange` (screen-reader text). |
| `className?` | `string` | none | Merged onto the wrapper. |

### Helpers (also exported from the package)

| Export | Description |
| --- | --- |
| `currencyDecimals(code)` | Decimals of a currency. Unknown codes give 2. |
| `parseMoney(text, { currency, locale?, paste?, overflow? })` | Text to minor units, or `null`. `paste: true` reads `1,250` as 1250 and `1.250,75` as 125075. |
| `formatMinor(minor, currency, locale, { digits?, fixed?, grouping? })` | Locale text without a symbol: `1,250.50`, `١٬٢٥٠٫٥٠`. |
| `minorToPlain(minor, decimals)` | Exact decimal string, `"-19.99"`. |
| `majorToMinor(major, currency)` / `minorToMajor(minor, currency)` | Conversions that avoid `1.005 * 100` mistakes. |
| `convertMinorDecimals(minor, from, to)` | Same money across currencies with different decimals. |
| `normalizeMoneyDigits(text)` | ٠-٩ and ۰-۹ to 0-9, Arabic marks to `.` and `,`. |
| `sanitizeMoneyText`, `toPlainDecimal`, `plainToMinor` | The editing pipeline, for custom fields. |
| `symbolSide(currency, locale)` / `currencySymbol(currency, locale, display?)` | Where and what the symbol is. |
| `moneyInRange(minor, min?, max?)` / `clampMoney(minor, min?, max?)` | Bounds. |

## Examples

### Arabic, Arabic digits and Saudi riyals

```tsx
import { CurrencyInput, NasaqProvider } from "@fadymondy/nasaq/web";

export const Riyals = () => (
  <NasaqProvider locale="ar" target="scope">
    <CurrencyInput aria-label="السعر" currency="SAR" numberingSystem="arab" defaultValue={125050} />
  </NasaqProvider>
);
```

The field shows `١٬٢٥٠٫٥٠` and the riyal symbol after the figure, as Arabic writes it.

### Choosable currency

```tsx
import { CurrencyInput } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Budget() {
  const [currency, setCurrency] = useState("USD");
  const [minor, setMinor] = useState<number | null>(1250);
  return (
    <CurrencyInput
      aria-label="Budget"
      currency={currency}
      currencies={["USD", "SAR", "KWD", "JPY"]}
      value={minor}
      onValueChange={setMinor}
      onCurrencyChange={(next, converted) => {
        setCurrency(next);
        setMinor(converted);
      }}
    />
  );
}
```

### In a Field with a range

```tsx
import { CurrencyInput, Field, FieldDescription, FieldLabel } from "@fadymondy/nasaq/web";

export const Deposit = () => (
  <Field>
    <FieldLabel>Deposit</FieldLabel>
    <CurrencyInput currency="SAR" min={5000} max={5000000} defaultValue={2500} />
    <FieldDescription>Between 50 and 50,000 SAR.</FieldDescription>
  </Field>
);
```

### Parse a pasted value yourself

```tsx
import { parseMoney } from "@fadymondy/nasaq/web";

const minor = parseMoney("١٬٢٥٠٫٥٠ ر.س.", { currency: "SAR", locale: "ar", paste: true }); // 125050
export { minor };
```

## Accessibility

| Key | Action |
| --- | --- |
| ArrowUp / ArrowDown | Add or subtract `step` (one major unit); Shift makes it 10 times larger. |
| Tab | Moves to the currency picker when there is one. |

- The input has `inputMode="decimal"` (`numeric` for 0 decimals), so phones show a number pad.
- Pass `aria-label`, or put the field inside a `Field` with a `FieldLabel`.
- An out-of-range amount sets `aria-invalid` and adds screen-reader text; the message beside the field is yours to write.

## RTL & i18n

- The input is always left to right (`dir="ltr"`): amounts read the same in Arabic and English. The symbol sits on the side the locale uses (`$1.00` at the start, `1.00 US$` at the end), inside a `bdi`.
- Type or paste `٠١٢٣٤٥٦٧٨٩`, `۰۱۲۳`, `٫` (Arabic decimal), `٬` (Arabic thousands) and `،` (Arabic comma): all read correctly. Display defaults to Western digits like the rest of Nasaq; pass `numberingSystem="arab"` for editorial forms.
- Typing treats `.` and the locale's decimal mark as the decimal point and drops other marks. Pasting is smarter: `1,250` is 1250, `1,25` (German) is 1.25.

## Styling & tokens

- Uses the `InputGroup` tokens (`--nq-control`, border, focus ring). The picker is a `Select` inside the group.
- `data-invalid` on the group when invalid or out of range. Extend with `className`.

## Do / Don't

- **Do** store and send the minor-unit integer.
- **Do** keep the currency next to the amount in your model: minor units mean nothing without it.
- **Don't** divide by 100 yourself: KWD has 3 decimals and JPY none. Use `currencyDecimals` or the helpers.
- **Don't** multiply floats (`19.99 * 100`); use `majorToMinor` or `parseMoney`.

## Related

- [Numeric](https://docs.nasaqui.com/components/numeric) · [Price](https://docs.nasaqui.com/components/price) · [PhoneInput](https://docs.nasaqui.com/components/phone-input) · [InputGroup](https://docs.nasaqui.com/components/input-group)

## Lab

https://docs.nasaqui.com/?path=/docs/components-forms-currency-input--docs

## Code

### React

```tsx
import { CurrencyInput } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Fee() {
  const [minor, setMinor] = useState<number | null>(1999);
  return <CurrencyInput aria-label="Fee" currency="USD" value={minor} onValueChange={setMinor} />;
}
```

### shadcn

```tsx
import { CurrencyInput } from "@/components/ui/currency-input";
import { useState } from "react";

export function Fee() {
  const [minor, setMinor] = useState<number | null>(1999);
  return <CurrencyInput aria-label="Fee" currency="USD" value={minor} onValueChange={setMinor} />;
}
```

### Vue

```vue
<script setup lang="ts">
import { NqCurrencyInput } from "@fadymondy/nasaq/vue";
import { ref } from "vue";

const minor = ref<number | null>(1999);
</script>

<template>
  <NqCurrencyInput v-model="minor" aria-label="Fee" currency="USD" />
</template>
```

### Blade

```blade
<div class="w-72">
    <x-nq::currency-input name="fee" :value="1999" currency="USD" :currencies="['USD', 'SAR', 'KWD']" aria-label="Fee" />
</div>
```

### HTML + Alpine

```html
<div class="w-72">
    <div data-slot="currency-input" data-currency="USD" x-data="nqCurrencyInput(1999, JSON.parse('{\u0022currency\u0022:\u0022USD\u0022,\u0022locale\u0022:\u0022en\u0022}'))" x-modelable="minor" x-id="['nq-currency']"
    class="flex w-full min-w-0 flex-col gap-1">
    <div role="group" data-slot="input-group"
    x-bind="group" class="group/input-group flex h-control min-h-[var(--nq-touch-min,0px)] w-full min-w-0 items-center overflow-hidden rounded-control border border-input bg-card text-body text-foreground transition-colors duration-150 ease-nq focus-within:border-nq-focus focus-within:outline-1 focus-within:outline-nq-focus has-[[data-invalid]]:border-nq-danger has-[[aria-invalid=true]]:border-nq-danger has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 pe-0"><div data-slot="currency-symbol" data-align="start" x-bind="symbolAddon"
            class="flex h-full shrink-0 items-center gap-1.5 text-body-sm text-muted-foreground [&amp;_svg]:size-4 order-first ps-3 pe-1">
            <span data-slot="input-group-text" class="select-none whitespace-nowrap"><bdi dir="ltr" x-text="mark()"></bdi></span>
        </div>
        <input data-slot="input-group-input" dir="ltr" x-bind="field" autocomplete="off" spellcheck="false"                           aria-label="Fee" class="h-full min-w-0 flex-1 border-0 bg-transparent px-3 text-body text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed pointer-coarse:text-[16px] text-start tabular-nums">
                    <div data-slot="select" x-data="nqSelect('USD', false)" x-modelable="value" x-id="['nq-select']" x-model="currency" class="contents">
    <button data-slot="select-trigger" x-ref="trigger" x-bind="trigger"
        aria-label="Currency" class="flex items-center justify-between gap-2 border-input px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] cursor-default select-none outline-none transition-colors duration-150 ease-nq focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-popup-open:border-nq-focus data-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-full w-auto min-w-20 rounded-none border-0 border-s bg-nq-surface-soft focus-visible:outline-offset-[-2px]">
    <span data-slot="select-value" class="min-w-0 flex-1 truncate text-start"><bdi dir="ltr" x-text="value">USD</bdi></span>
    <span class="flex shrink-0 text-muted-foreground [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg></span>
</button>
                <template x-teleport="body">
    <div data-slot="select-content" x-ref="popup" x-bind="popup" x-nq-presence="open" x-anchor.bottom-start.offset.4="$refs.trigger"
        class="z-50 min-w-[var(--anchor-width)] max-h-[var(--available-height)] overflow-y-auto rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
        <div data-slot="select-item" x-bind="item('USD', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('USD')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate"><bdi dir="ltr">USD</bdi></span>
</div>
                                            <div data-slot="select-item" x-bind="item('SAR', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('SAR')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate"><bdi dir="ltr">SAR</bdi></span>
</div>
                                            <div data-slot="select-item" x-bind="item('KWD', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('KWD')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate"><bdi dir="ltr">KWD</bdi></span>
</div>
    </div>
</template>
    </div></div>
    <span x-show="out()" x-cloak style="display: none" :id="rangeId()" class="sr-only">Amount out of range</span>
            <input type="hidden" name="fee" :value="minor === null ? '' : String(minor)">
    </div>
</div>
```
