# Number stepper
> Integer stepper whose number rolls over a dotted wash; holding a button repeats faster, and stepping past a limit shakes and flashes red.
- Element: `<hf-number>`
- React: `import { HfNumber } from "@/components/lumesec/hf-number"`
- Collection: Pixel HD (https://elements.lumesec.ai/components/pixel-hd)
- Registry item: https://elements.lumesec.ai/r/hf-number.json
- Page: https://elements.lumesec.ai/components/pixel-hd/number



Live preview: https://elements.lumesec.ai/view/hf-number

```html
<hf-number name="seats" label="Seats" value="12" min="1" max="50" price="24"></hf-number>
```

## Playground

Change a prop and the component updates. Props marked live animate to the new value; the others rebuild the element.

## Installation

```bash
npx shadcn@latest add @lumesec/hf-number
```

First time with the @lumesec registry? Register it once, or install by URL:

```bash
npx shadcn@latest registry add @lumesec=https://elements.lumesec.ai/r/{name}.json
npx shadcn@latest add https://elements.lumesec.ai/r/hf-number.json
```

## Usage

React:

```tsx
import { HfNumber } from "@/components/lumesec/hf-number";

export function Example() {
  return (
    <HfNumber name="seats" label="Seats" value={12} min={1} max={50} price={24} />
  );
}
```

HTML:

```html
<script type="module" src="https://elements.lumesec.ai/cdn/hf-number.js"></script>

<hf-number name="seats" label="Seats" value="12" min="1" max="50" price="24"></hf-number>
```

## Behaviour

The number rolls up or down over a dotted wash, and a scan line crosses the dots on every step. Hold a button to repeat faster, with heat and sparks building up; bumping the limit flashes red. Click the number to type.

## API reference

### Attributes

| Attribute  | React prop | Type     | Default | Description                                                                                                                |
| ---------- | ---------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------- |
| `value`    | `value`    | `number` | `5`     | Current value, rounded and clamped to `min`–`max`. Observed: a change rolls to the new number. Reflects the current value. |
| `min`      | `min`      | `number` | `1`     | Lowest allowed value.                                                                                                      |
| `max`      | `max`      | `number` | `50`    | Highest allowed value.                                                                                                     |
| `label`    | `label`    | `string` | `Seats` | Label text, also used as the field's `aria-label`.                                                                         |
| `price`    | `price`    | `number` | —       | Unit price. When set, the line below reads `<value> × <currency><price> = <currency><total> / month`.                      |
| `currency` | `currency` | `string` | `€`     | Currency symbol used with `price`.                                                                                         |
| `unit`     | `unit`     | `string` | —       | Text shown below the stepper when `price` is not set.                                                                      |
| `name`     | `name`     | `string` | —       | Form field name used when the element is inside a form.                                                                    |

### Events

Events bubble and cross the shadow boundary unless the description says otherwise.

| Event    | React prop | Detail              | Description                                                                        |
| -------- | ---------- | ------------------- | ---------------------------------------------------------------------------------- |
| `input`  | `onInput`  | `{ value: number }` | Fires on each change from the buttons, keys, typed entry or the `value` attribute. |
| `change` | `onChange` | `{ value: number }` | Fires 300 ms after the last `input`, so holding a button produces one `change`.    |

### Properties

| Property           | Type                      | Description                                                          |
| ------------------ | ------------------------- | -------------------------------------------------------------------- |
| `value`            | `number`                  | Current value. Setting it writes the `value` attribute.              |
| `min` (read-only)  | `number`                  | The `min` attribute, or 1.                                           |
| `max` (read-only)  | `number`                  | The `max` attribute, or 50.                                          |
| `form` (read-only) | `HTMLFormElement \| null` | The owning form, from `ElementInternals`.                            |
| `name` (read-only) | `string \| null`          | The `name` attribute. There is no setter; set the attribute instead. |

## Keyboard

| Keys                | Action                                                                                                                 |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| ArrowUp / ArrowDown | In the number field: add or subtract 1.                                                                                |
| PageUp / PageDown   | In the number field: add or subtract 10. If that would pass `min` or `max`, the value stays and the field flashes red. |
| Enter               | In the number field: commit the typed number and leave the field.                                                      |
| Enter / Space       | On the − or + button: step by 1.                                                                                       |

## Accessibility

* The number is an `<input>` with `role="spinbutton"`, `inputmode="numeric"`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax` and an `aria-label` from `label`.
* The − and + buttons have `aria-label` `Decrease` and `Increase` and are disabled at the limits.
* The price or unit line below is an `aria-live="polite"` region.
* The visible number is text marked `aria-hidden`, because the spinbutton already exposes the value; while the input has focus, its own text replaces it. The wash and sparks are on an `aria-hidden` canvas.
* Form-associated through `ElementInternals`.
* Submits the integer as a string under `name`. There is no validation, and form reset and `disabled` are not handled.
* Reduced motion: the number and the line below change without rolling, and the scan line, shake and sparks are skipped; the red flash and heat tint remain.

## Theming

The element reads your shadcn theme tokens through its shadow root, so light and dark follow your theme. The accent comes from `--lumesec`. See [Theming](/docs/theming).

This component reads `--border`, `--card`, `--foreground`, `--lumesec`, `--muted` and `--muted-foreground`.

To restyle only LumeSec elements, set the matching `--ui-*` overrides: `--ui-accent`, `--ui-border`, `--ui-fg`, `--ui-mono`, `--ui-muted`, `--ui-raised` and `--ui-surface`.

## Notes

* Holding − or + repeats after 420 ms, speeding up from 160 ms to 35 ms per step. While a button is held, the number and the line below swap without rolling.
* Clicking the number focuses the field for typing. The typed text is parsed as an integer and clamped when the field loses focus.
* Programmatic changes to `value` fire `input` and `change`, the same as user changes.
* While typing, the inner field's native `input` events stay inside the shadow root; only the element's own `input` and `change` events, with `value` in `detail`, reach the host.
* The host is `inline-block`; the buttons and number area are 40px tall. The number is set in the mono font with tabular figures.


