# Number input

A field for one number, with buttons to step it down and up. Arrow Up and Arrow Down step it from the keyboard, a screen reader hears a spin button with its value, and the phone shows a number pad. Empty is `null`, never 0: nothing is filled in for the person.

The number is read and shown in LocaleProvider's locale, not the browser's: "1,5" is one and a half in German, and digits in any script (Arabic-Indic, Persian, Devanagari, Bengali …) are read too. What the person types stays as typed until they leave the field, unless the value changes from outside to a different number, or to `null` from a number. Text that isn't a number is already `null`, so only its form's reset clears it (an uncontrolled field then goes back to `defaultValue`). With `name`, a form gets the number in plain digits ("1.5"), whatever the locale.

The step buttons are for a pointer and stay out of the tab order (the arrow keys do the same from the field). At `min` or `max` the button that would pass it is locked and says why (in a tooltip: needs a `TooltipProvider`), and an arrow key pressed at the limit says the same to a screen reader. A field that is `disabled`, `readOnly` or `aria-disabled` (as `Locked` makes it) doesn't change by any route: the step buttons, the arrow keys and the `scrub` grip do nothing, the buttons are `aria-disabled` and described by what describes the field (`Locked`'s reason), and an `aria-disabled` field takes no typing either. Values typed outside the range are kept as typed, for your validation to name; the buttons and arrow keys stop at the range. From empty, the first step goes to 0, or to the nearest end of the range when 0 is outside it.

Range checks are your app's. The field is a text field, so it carries no `min`, `max` or `step` for the browser: `form.checkValidity()`, `:invalid` and `:out-of-range` don't catch an out-of-range number. Check the number you get from `onValueChange` and give `Field` the error.

Use `Field` for the label, hint and error, and `unit` for what the number counts ("minutes", "USD"); the unit is read with the field. A phone's number pad has no minus sign, so with a `min` below 0 (or none) the field asks for the full keyboard.

- Registry name: `number-input` (`@candor/number-input`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/number-input

## Install

```bash
npx candor-ui add number-input
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/number-input`.
It brings in: button, input, locale, locked, motion, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/candor/ui/card"
import { Field } from "@/components/candor/ui/field"
import { NumberInput } from "@/components/candor/ui/number-input"
import { TooltipProvider } from "@/components/candor/ui/tooltip"

// Empty is null, not 0. The buttons stop at the range and say why; a typed value outside it is left for your check to name.
function TimeLimit() {
  const [minutes, setMinutes] = React.useState<number | null>(15)
  const error = minutes !== null && (minutes < 1 || minutes > 120) ? "Enter a time limit from 1 to 120 minutes" : undefined
  return (
    <Field label="Time limit per run" hint="The agent stops and reports what it finished." error={error} className="w-full max-w-xs">
      <NumberInput value={minutes} onValueChange={setMinutes} min={1} max={120} step={5} unit="minutes" />
    </Field>
  )
}

// A fine-tune card: an inspector built from Card and NumberInputs with `scrub`. Drag a grip sideways to change its
// number (Shift for ten steps at a time); the arrow keys and the step buttons do the same from the keyboard.
function FineTune() {
  const [temperature, setTemperature] = React.useState<number | null>(0.7)
  const [tokens, setTokens] = React.useState<number | null>(1200)
  const [retries, setRetries] = React.useState<number | null>(2)
  return (
    <Card className="w-full max-w-xs">
      <CardHeader>
        <CardTitle>Fine-tune</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-3">
        <Field label="Temperature">
          <NumberInput value={temperature} onValueChange={setTemperature} min={0} max={2} step={0.1} scrub />
        </Field>
        <Field label="Reply length">
          <NumberInput value={tokens} onValueChange={setTokens} min={100} max={8000} step={100} unit="tokens" scrub />
        </Field>
        <Field label="Retries">
          <NumberInput value={retries} onValueChange={setRetries} min={0} max={5} scrub />
        </Field>
      </CardContent>
    </Card>
  )
}

export default function Example() {
  return (
    <TooltipProvider>
      <div className="flex w-full flex-col gap-6">
        <TimeLimit />
        <FineTune />
      </div>
    </TooltipProvider>
  )
}
```

## <NumberInput>

Props:

- `value?: number | null | undefined`: The number, when you control it. `null` is empty.
- `defaultValue?: number | undefined`: The number at first, when you don't control it. Left out, the field is empty.
- `onValueChange?: ((value: number | null) => void) | undefined`: Called with the number as the person types or steps, or `null` when the field is emptied or what is typed isn't a number.
- `min?: number | undefined`: The lowest the buttons and arrow keys go. 0 or more also brings up the phone's number pad. A typed number below it is your validation's to catch: the browser isn't told.
- `max?: number | undefined`: The highest the buttons and arrow keys go. A typed number above it is your validation's to catch: the browser isn't told.
- `step?: number | undefined` (default `1`): How much a button press or arrow key changes the number. Defaults to 1.
- `unit?: string | undefined`: What the number counts, printed after it and read with the field: "minutes", "USD".
- `attend?: boolean | undefined`: Rings the field once, as `Input`'s `attend` does, after a failed submit.
- `scrub?: boolean | undefined` (default `false`): Adds a grip at the field's start: dragging it sideways changes the number by a step for every 4 pixels (ten steps with Shift), towards the end of the line to raise it (the left in a right-to-left page), and stops at `min` and `max`. For an inspector's fine tuning; the arrow keys and the step buttons stay the way to it for the keyboard and screen readers, and the grip is hidden from them.
- `name?: string | undefined`: The form field's name. The form gets the number in plain digits ("1.5"), or "" when empty.
- `"aria-describedby"?: string | undefined`: Ids of anything else that describes the field; the unit's id is added after them.

Also takes the props of `Omit<React.ComponentProps<"input">, "value" | "defaultValue" | "type" | "onChange" | "min" | "max" | "step" | "inputMode">`.

## Strings (`numberInput`)

The fixed words, English by default. Override any of them under `numberInput` in `<LocaleProvider strings={{ numberInput: { … } }}>`; the table is exported as `numberInputStrings`.

- `decrease`: Decrease
- `increase`: Increase
- `atMin`: Already at the lowest, {min}.
- `atMax`: Already at the highest, {max}.

## Motion

- **scrub** (clock): With `scrub`, while the person drags the grip sideways the number follows the pointer, a step for every 4 pixels, painted from the pointer itself, with no motion between steps.
- **value** (value, fade): When the person presses a step button and the number changes, the field's number crossfades to the new one, carrying which way it went for the Pro number drawings. Typing, the arrow keys and a field drawn with its value stay still, and `attend` clearing doesn't replay the last step's.

Set with `--cd-motion-number-input-<moment>-*` in your brand file; the families are in the design reference.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
