{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-input",
  "title": "Number Input",
  "description": "A spinbutton with min, max, step and locale formatting.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/number-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { MinusIcon, PlusIcon } from \"lucide-react\"\n\nimport { cn } from \"@/registry/default/lib/utils\"\n\nconst LARGE_STEP_MULTIPLIER = 10\n\nfunction decimalPlaces(step: number) {\n  const [, fraction = \"\"] = String(step).split(\".\")\n  return fraction.length\n}\n\nfunction clampToRange(value: number, min?: number, max?: number) {\n  return Math.min(max ?? Infinity, Math.max(min ?? -Infinity, value))\n}\n\nfunction separatorsFor(locale?: string) {\n  const parts = new Intl.NumberFormat(locale).formatToParts(12345.6)\n  return {\n    group: parts.find((part) => part.type === \"group\")?.value ?? \",\",\n    decimal: parts.find((part) => part.type === \"decimal\")?.value ?? \".\",\n  }\n}\n\nfunction parseLocalizedNumber(\n  text: string,\n  locale?: string,\n  formatOptions?: Intl.NumberFormatOptions\n) {\n  const { group, decimal } = separatorsFor(locale)\n  const normalized = text\n    .split(group)\n    .join(\"\")\n    .replace(decimal, \".\")\n    .replace(/[^\\d.-]/g, \"\")\n  if (normalized === \"\" || normalized === \"-\") return null\n\n  const parsed = Number(normalized)\n  if (Number.isNaN(parsed)) return null\n\n  return formatOptions?.style === \"percent\" ? parsed / 100 : parsed\n}\n\nfunction NumberInput({\n  value,\n  defaultValue = null,\n  onValueChange,\n  min,\n  max,\n  step = 1,\n  locale,\n  formatOptions,\n  name,\n  disabled,\n  className,\n  onBlur,\n  onKeyDown,\n  ...props\n}: Omit<\n  React.ComponentProps<\"input\">,\n  \"value\" | \"defaultValue\" | \"type\" | \"min\" | \"max\" | \"step\" | \"onChange\"\n> & {\n  value?: number | null\n  defaultValue?: number | null\n  onValueChange?: (value: number | null) => void\n  min?: number\n  max?: number\n  step?: number\n  locale?: string\n  formatOptions?: Intl.NumberFormatOptions\n}) {\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)\n  const [draft, setDraft] = React.useState<string | null>(null)\n  const currentValue = value !== undefined ? value : uncontrolledValue\n  const formatter = new Intl.NumberFormat(locale, formatOptions)\n  const displayedText =\n    draft ?? (currentValue === null ? \"\" : formatter.format(currentValue))\n  const canDecrement =\n    currentValue === null || currentValue > (min ?? -Infinity)\n  const canIncrement = currentValue === null || currentValue < (max ?? Infinity)\n\n  function commit(next: number | null) {\n    const settled = next === null ? null : clampToRange(next, min, max)\n    setDraft(null)\n    if (value === undefined) setUncontrolledValue(settled)\n    if (settled !== currentValue) onValueChange?.(settled)\n  }\n\n  function committedOrDraftValue() {\n    if (draft === null) return currentValue\n    return parseLocalizedNumber(draft, locale, formatOptions)\n  }\n\n  function stepBy(amount: number) {\n    const base = committedOrDraftValue() ?? min ?? 0\n    const precision = Math.max(decimalPlaces(base), decimalPlaces(step))\n    commit(Number((base + amount).toFixed(precision)))\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n    onKeyDown?.(event)\n    if (event.defaultPrevented) return\n\n    const largeStep = step * LARGE_STEP_MULTIPLIER\n    const stepAmount = event.shiftKey ? largeStep : step\n\n    switch (event.key) {\n      case \"ArrowUp\":\n        stepBy(stepAmount)\n        break\n      case \"ArrowDown\":\n        stepBy(-stepAmount)\n        break\n      case \"PageUp\":\n        stepBy(largeStep)\n        break\n      case \"PageDown\":\n        stepBy(-largeStep)\n        break\n      case \"Home\":\n        if (min === undefined) return\n        commit(min)\n        break\n      case \"End\":\n        if (max === undefined) return\n        commit(max)\n        break\n      case \"Enter\":\n        commit(committedOrDraftValue())\n        return\n      default:\n        return\n    }\n    event.preventDefault()\n  }\n\n  return (\n    <div\n      data-slot=\"number-input\"\n      data-disabled={disabled ? \"\" : undefined}\n      className={cn(\n        \"flex h-9 w-full min-w-0 items-stretch overflow-hidden rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] dark:bg-input/30\",\n        \"focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50\",\n        \"has-[input[aria-invalid=true]]:border-destructive has-[input[aria-invalid=true]]:ring-destructive/20\",\n        \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n        className\n      )}\n    >\n      <StepButton\n        label=\"Decrease\"\n        disabled={disabled || !canDecrement}\n        onStep={() => stepBy(-step)}\n      >\n        <MinusIcon />\n      </StepButton>\n      <input\n        type=\"text\"\n        inputMode=\"decimal\"\n        autoComplete=\"off\"\n        role=\"spinbutton\"\n        aria-valuenow={currentValue ?? undefined}\n        aria-valuemin={min}\n        aria-valuemax={max}\n        disabled={disabled}\n        value={displayedText}\n        onChange={(event) => setDraft(event.target.value)}\n        onBlur={(event) => {\n          onBlur?.(event)\n          if (draft !== null) commit(committedOrDraftValue())\n        }}\n        onKeyDown={handleKeyDown}\n        className=\"w-full min-w-0 flex-1 border-x border-input bg-transparent px-2 text-center text-base tabular-nums outline-none placeholder:text-muted-foreground md:text-sm\"\n        {...props}\n      />\n      <StepButton\n        label=\"Increase\"\n        disabled={disabled || !canIncrement}\n        onStep={() => stepBy(step)}\n      >\n        <PlusIcon />\n      </StepButton>\n      {name && <input type=\"hidden\" name={name} value={currentValue ?? \"\"} />}\n    </div>\n  )\n}\n\nfunction StepButton({\n  label,\n  disabled,\n  onStep,\n  children,\n}: {\n  label: string\n  disabled?: boolean\n  onStep: () => void\n  children: React.ReactNode\n}) {\n  return (\n    <button\n      type=\"button\"\n      tabIndex={-1}\n      aria-label={label}\n      disabled={disabled}\n      onPointerDown={(event) => event.preventDefault()}\n      onClick={onStep}\n      className=\"flex w-9 shrink-0 items-center justify-center text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40 [&_svg]:size-3.5\"\n    >\n      {children}\n    </button>\n  )\n}\n\nexport { NumberInput }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}