{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-field",
  "title": "Number Field",
  "description": "Numeric stepper with hold-to-repeat, keyboard steps and locale formatting.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/number-field.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { MinusIcon, PlusIcon } from \"lucide-react\";\n\nimport { gsap, motionScale } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype NumberFieldProps = Omit<React.ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"onChange\" | \"size\" | \"type\" | \"min\" | \"max\" | \"step\"> & {\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (value: number) => void;\n  min?: number;\n  max?: number;\n  step?: number;\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Display formatting, e.g. `{ style: \"currency\", currency: \"USD\" }`. */\n  formatOptions?: Intl.NumberFormatOptions;\n};\n\nconst heights = { sm: \"h-ctl-sm\", md: \"h-ctl-md\", lg: \"h-ctl-lg\" };\n\n/**\n * A numeric stepper: −/+ buttons (press and hold to repeat, accelerating), arrow keys, Page Up/Down for ×10,\n * Home/End for min/max. The value slides in from the direction it moved.\n */\nfunction NumberField({ className, value, defaultValue = 0, onValueChange, min = -Infinity, max = Infinity, step = 1, size = \"md\", formatOptions, disabled, ...props }: NumberFieldProps) {\n  const [internal, setInternal] = React.useState(defaultValue);\n  const current = value ?? internal;\n  const [draft, setDraft] = React.useState<string | null>(null);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const repeat = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n  const latest = React.useRef(current);\n  latest.current = current;\n\n  const format = React.useMemo(() => new Intl.NumberFormat(undefined, formatOptions), [formatOptions]);\n  const clamp = (n: number) => Math.min(max, Math.max(min, n));\n\n  const commit = (next: number) => {\n    const clamped = clamp(Number(next.toFixed(10)));\n    if (clamped === latest.current) return;\n    const direction = clamped > latest.current ? 1 : -1;\n    latest.current = clamped;\n    setInternal(clamped);\n    onValueChange?.(clamped);\n    const el = inputRef.current;\n    const scale = el ? motionScale(el) : 0;\n    if (el && scale) gsap.fromTo(el, { y: 8 * direction * scale, opacity: 0.2 }, { y: 0, opacity: 1, duration: 0.3, ease: \"cue\", overwrite: true });\n  };\n\n  const stopRepeat = () => {\n    if (repeat.current) clearTimeout(repeat.current);\n    repeat.current = null;\n  };\n  const startRepeat = (delta: number) => {\n    commit(latest.current + delta);\n    let delay = 380;\n    const tick = () => {\n      commit(latest.current + delta);\n      delay = Math.max(40, delay * 0.8);\n      repeat.current = setTimeout(tick, delay);\n    };\n    repeat.current = setTimeout(tick, delay);\n  };\n  React.useEffect(() => stopRepeat, []);\n\n  const stepper = (delta: number, label: string, Icon: typeof PlusIcon) => (\n    <button\n      type=\"button\"\n      tabIndex={-1}\n      aria-label={label}\n      disabled={disabled || (delta > 0 ? current >= max : current <= min)}\n      onPointerDown={(e) => {\n        e.preventDefault();\n        startRepeat(delta);\n      }}\n      onPointerUp={stopRepeat}\n      onPointerLeave={stopRepeat}\n      className={cn(\"inline-flex aspect-square h-full items-center justify-center text-muted-foreground transition-colors hover:bg-muted hover:text-foreground active:bg-accent disabled:pointer-events-none disabled:opacity-40\", heights[size])}\n    >\n      <Icon className=\"size-3.5\" />\n    </button>\n  );\n\n  return (\n    <div\n      data-slot=\"number-field\"\n      className={cn(\n        \"inline-flex w-36 items-stretch overflow-hidden rounded-md border border-input bg-card shadow-well transition-[border-color,box-shadow] duration-150 ease-cue has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-ring/20 has-[input:disabled]:opacity-50\",\n        heights[size],\n        className,\n      )}\n    >\n      {stepper(-step, \"Decrease\", MinusIcon)}\n      <input\n        ref={inputRef}\n        data-slot=\"number-field-input\"\n        type=\"text\"\n        inputMode=\"decimal\"\n        role=\"spinbutton\"\n        aria-valuenow={current}\n        aria-valuemin={Number.isFinite(min) ? min : undefined}\n        aria-valuemax={Number.isFinite(max) ? max : undefined}\n        disabled={disabled}\n        value={draft ?? format.format(current)}\n        onChange={(e) => setDraft(e.target.value)}\n        onFocus={() => setDraft(String(current))}\n        onBlur={() => {\n          const parsed = Number.parseFloat((draft ?? \"\").replace(/[^\\d.-]/g, \"\"));\n          if (Number.isFinite(parsed)) commit(parsed);\n          setDraft(null);\n        }}\n        onKeyDown={(e) => {\n          const keys: Record<string, number> = { ArrowUp: step, ArrowDown: -step, PageUp: step * 10, PageDown: -step * 10 };\n          if (e.key in keys) {\n            e.preventDefault();\n            commit(current + keys[e.key]!);\n            setDraft(null);\n          } else if (e.key === \"Home\" && Number.isFinite(min)) commit(min);\n          else if (e.key === \"End\" && Number.isFinite(max)) commit(max);\n          else if (e.key === \"Enter\") e.currentTarget.blur();\n        }}\n        className=\"w-full min-w-0 flex-1 border-x border-input bg-transparent text-center text-sm font-medium tabular-nums outline-none\"\n        {...props}\n      />\n      {stepper(step, \"Increase\", PlusIcon)}\n    </div>\n  );\n}\n\nexport { NumberField };\nexport type { NumberFieldProps };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/number-field",
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}