{"$schema":"https://ui.logic2b.com/schema/registry-item.json","name":"number-field","type":"registry:ui","title":"Number Field","description":"A numeric input with decrement/increment steppers, min/max clamping and step. Controlled via value/onValueChange or uncontrolled via defaultValue. Renders a real <input type=number> (native form submission + keyboard) with the native spinners hidden in favor of the styled buttons.","dependencies":["lucide-react"],"registryDependencies":["utils"],"files":[{"path":"ui/number-field.tsx","type":"registry:ui","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { MinusIcon, PlusIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface NumberFieldProps\n  extends Omit<\n    React.ComponentProps<\"input\">,\n    \"value\" | \"defaultValue\" | \"onChange\" | \"type\"\n  > {\n  value?: number\n  defaultValue?: number\n  onValueChange?: (value: number) => void\n  min?: number\n  max?: number\n  step?: number\n}\n\n/**\n * A numeric input with decrement/increment steppers, min/max clamping and step.\n * Controlled via `value`/`onValueChange` or uncontrolled via `defaultValue`.\n * Renders a real `<input type=\"number\">` so native form submission and keyboard\n * work; the native spinners are hidden in favor of the styled buttons.\n */\nfunction NumberField({\n  className,\n  value,\n  defaultValue,\n  onValueChange,\n  min,\n  max,\n  step = 1,\n  disabled,\n  ...props\n}: NumberFieldProps) {\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState(defaultValue ?? 0)\n  const current = isControlled ? (value as number) : internal\n\n  const clamp = (n: number) => {\n    if (min !== undefined) n = Math.max(min, n)\n    if (max !== undefined) n = Math.min(max, n)\n    return n\n  }\n\n  const commit = (n: number) => {\n    const next = Number.isNaN(n) ? NaN : clamp(n)\n    if (!isControlled) setInternal(next)\n    onValueChange?.(next)\n  }\n\n  const stepBy = (direction: 1 | -1) => {\n    const base = Number.isNaN(current) ? (min ?? 0) : current\n    // Trim floating-point noise from repeated steps (0.1 + 0.2 …).\n    const raw = base + direction * step\n    commit(Math.round((raw + Number.EPSILON) * 1e6) / 1e6)\n  }\n\n  const atMin = min !== undefined && !Number.isNaN(current) && current <= min\n  const atMax = max !== undefined && !Number.isNaN(current) && current >= max\n\n  const buttonClass =\n    \"flex h-full aspect-square items-center justify-center text-muted-foreground transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-40 focus-visible:text-foreground outline-none\"\n\n  return (\n    <div\n      data-slot=\"number-field\"\n      data-disabled={disabled ? \"\" : undefined}\n      className={cn(\n        \"border-input focus-within:border-ring focus-within:ring-ring/50 inline-flex h-9 items-center rounded-md border bg-transparent shadow-xs transition-[color,box-shadow] focus-within:ring-[3px]\",\n        \"data-disabled:pointer-events-none data-disabled:opacity-50\",\n        className\n      )}\n    >\n      <button\n        type=\"button\"\n        data-slot=\"number-field-decrement\"\n        aria-label=\"Decrement\"\n        tabIndex={-1}\n        disabled={disabled || atMin}\n        onClick={() => stepBy(-1)}\n        className={cn(buttonClass, \"border-r\")}\n      >\n        <MinusIcon className=\"size-4\" />\n      </button>\n      <input\n        type=\"number\"\n        inputMode=\"decimal\"\n        data-slot=\"number-field-input\"\n        disabled={disabled}\n        min={min}\n        max={max}\n        step={step}\n        value={Number.isNaN(current) ? \"\" : current}\n        onChange={(e) =>\n          commit(e.target.value === \"\" ? NaN : Number(e.target.value))\n        }\n        className={cn(\n          \"h-full w-16 min-w-0 bg-transparent text-center text-sm tabular-nums outline-none\",\n          \"[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none\"\n        )}\n        {...props}\n      />\n      <button\n        type=\"button\"\n        data-slot=\"number-field-increment\"\n        aria-label=\"Increment\"\n        tabIndex={-1}\n        disabled={disabled || atMax}\n        onClick={() => stepBy(1)}\n        className={cn(buttonClass, \"border-l\")}\n      >\n        <PlusIcon className=\"size-4\" />\n      </button>\n    </div>\n  )\n}\n\nexport { NumberField }\n"}],"categories":["form"],"accessibility":{"support":"native","pattern":"spinbutton","primitive":"HTMLInputElement type=number","keyboard":[{"keys":["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"],"action":"Move within the composite control."},{"keys":["Enter"],"action":"Commit typed input according to native number-field behavior."}],"aria":["The input retains native spinbutton semantics; step buttons are native buttons with labels."],"consumer":["Associate the input with a label and communicate min/max constraints in visible help text."]},"api":{"source":"src/ui/number-field.tsx","exports":[{"name":"NumberFieldProps","kind":"type","definition":"interface NumberFieldProps extends Omit< React.ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"onChange\" | \"type\" > { value?: number defaultValue?: number onValueChange?: (value: number) => void min?: number max?: number step?: number }"},{"name":"NumberField","kind":"component","description":"A numeric input with decrement/increment steppers, min/max clamping and step. Controlled via `value`/`onValueChange` or uncontrolled via `defaultValue`. Renders a real `<input type=\"number\">` so native form submission and keyboard work; the native spinners are hidden in favor of the styled buttons.","propsType":"NumberFieldProps","props":[{"name":"value","type":"number","required":false},{"name":"defaultValue","type":"number","required":false},{"name":"onValueChange","type":"(value: number) => void","required":false},{"name":"min","type":"number","required":false},{"name":"max","type":"number","required":false},{"name":"step","type":"number","required":false,"default":"1"}]}]}}