{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-input",
  "type": "registry:ui",
  "title": "Number Input",
  "description": "Quantity stepper with plus/minus buttons around a numeric field.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/number-input.tsx",
      "content": "import * as React from 'react'\nimport { Minus, Plus } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\nexport interface NumberInputProps {\n  value?: number\n  defaultValue?: number\n  min?: number\n  max?: number\n  step?: number\n  onChange?: (value: number) => void\n  label?: React.ReactNode\n  className?: string\n  disabled?: boolean\n  ariaLabel?: string\n}\n\nexport function NumberInput({\n  value,\n  defaultValue = 1,\n  min = -Infinity,\n  max = Infinity,\n  step = 1,\n  onChange,\n  label,\n  className,\n  disabled = false,\n  ariaLabel = 'Quantity',\n}: NumberInputProps) {\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState(defaultValue)\n  const current = isControlled ? (value as number) : internal\n\n  const clamp = (next: number) => {\n    if (Number.isNaN(next)) return current\n    return Math.min(max, Math.max(min, next))\n  }\n\n  const commit = (next: number) => {\n    const clamped = clamp(next)\n    if (!isControlled) setInternal(clamped)\n    onChange?.(clamped)\n  }\n\n  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const parsed = Number(event.target.value)\n    if (event.target.value === '') {\n      if (!isControlled) setInternal(0)\n      onChange?.(0)\n      return\n    }\n    if (Number.isNaN(parsed)) return\n    const clamped = clamp(parsed)\n    if (!isControlled) setInternal(clamped)\n    onChange?.(clamped)\n  }\n\n  const stepButtonClass = (side: 'left' | 'right') =>\n    cn(\n      'flex h-9 w-9 shrink-0 items-center justify-center bg-rk-surface font-black text-rk-ink rk-border-sm transition-all cursor-pointer select-none',\n      'hover:bg-rk-primary active:translate-y-[2px] active:shadow-none',\n      'focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',\n      side === 'left' ? 'rounded-l-full' : 'rounded-r-full',\n      (disabled || (side === 'left' && current <= min) || (side === 'right' && current >= max)) &&\n        'pointer-events-none opacity-40'\n    )\n\n  return (\n    <div className={cn('inline-flex flex-col gap-1.5 font-sans', className)}>\n      {label && (\n        <label className=\"text-xs font-extrabold text-rk-ink/80\">{label}</label>\n      )}\n      <div className=\"inline-flex items-stretch\">\n        <button\n          type=\"button\"\n          aria-label=\"Decrease value\"\n          onClick={() => commit(current - step)}\n          disabled={disabled || current <= min}\n          className={stepButtonClass('left')}\n        >\n          <Minus className=\"h-4 w-4 stroke-[3]\" aria-hidden=\"true\" />\n        </button>\n        <input\n          type=\"text\"\n          inputMode=\"numeric\"\n          aria-label={ariaLabel}\n          value={current}\n          disabled={disabled}\n          onChange={handleChange}\n          className=\"h-9 w-16 border-x-0 border-y-0 bg-rk-surface text-center font-mono text-sm font-black text-rk-ink outline-none focus-visible:bg-rk-primary/40 disabled:opacity-50\"\n        />\n        <button\n          type=\"button\"\n          aria-label=\"Increase value\"\n          onClick={() => commit(current + step)}\n          disabled={disabled || current >= max}\n          className={stepButtonClass('right')}\n        >\n          <Plus className=\"h-4 w-4 stroke-[3]\" aria-hidden=\"true\" />\n        </button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/number-input.tsx"
    }
  ]
}