{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "correct-number-input",
  "type": "registry:ui",
  "title": "Correct Number Input",
  "description": "iOS 26 liquid-glass number input with floating label, integrated stepper buttons, and haptic sound.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/correct-number-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion } from \"motion/react\";\n\n/* ── sound ── */\nlet _a: AudioContext, _b: AudioBuffer;\nconst tick = () => {\n  if (typeof window === \"undefined\") return;\n  if (!_a) {\n    _a = new AudioContext();\n    _b = _a.createBuffer(1, (_a.sampleRate * 0.003) | 0, _a.sampleRate);\n    const d = _b.getChannelData(0);\n    for (let i = 0; i < d.length; i++)\n      d[i] = (Math.random() * 2 - 1) * (1 - i / d.length) ** 4;\n  }\n  const s = _a.createBufferSource();\n  s.buffer = _b;\n  const g = _a.createGain();\n  g.gain.value = 0.08;\n  s.connect(g).connect(_a.destination);\n  s.start();\n};\n\n/* ── theme ── */\nconst CSS = `\n.ni{\n  --ni-glass:linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0.62));\n  --ni-border:rgba(0,0,0,0.06);\n  --ni-shadow:0 0 1px rgba(0,0,0,0.04),0 2px 8px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.8);\n  --ni-dim:rgba(0,0,0,0.42);\n  --ni-mid:rgba(0,0,0,0.55);\n  --ni-hi:rgba(0,0,0,0.88);\n  --ni-focus:rgba(0,0,0,0.12);\n  --ni-btn:rgba(0,0,0,0.03);\n  --ni-btn-h:rgba(0,0,0,0.07);\n  --ni-sep:rgba(0,0,0,0.06)\n}\n.dark .ni,[data-theme=\"dark\"] .ni{\n  --ni-glass:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));\n  --ni-border:rgba(255,255,255,0.07);\n  --ni-shadow:0 1px 3px rgba(0,0,0,0.08),inset 0 1px 0 rgba(255,255,255,0.04);\n  --ni-dim:rgba(255,255,255,0.28);\n  --ni-mid:rgba(255,255,255,0.5);\n  --ni-hi:rgba(255,255,255,0.88);\n  --ni-focus:rgba(255,255,255,0.12);\n  --ni-btn:rgba(255,255,255,0.03);\n  --ni-btn-h:rgba(255,255,255,0.07);\n  --ni-sep:rgba(255,255,255,0.06)\n}`;\n\n/* ── types ── */\ninterface CorrectNumberInputProps {\n  label?: string;\n  hint?: string;\n  defaultValue?: number;\n  min?: number;\n  max?: number;\n  step?: number;\n  onChange?: (value: number) => void;\n  sound?: boolean;\n  style?: React.CSSProperties;\n}\n\n/* ── component ── */\nexport default function CorrectNumberInput({\n  label = \"Amount\",\n  hint,\n  defaultValue = 0,\n  min,\n  max,\n  step = 1,\n  onChange,\n  sound = true,\n  style,\n}: CorrectNumberInputProps) {\n  const [value, setValue] = React.useState(defaultValue);\n  const [focused, setFocused] = React.useState(false);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const hasValue = value !== 0 || focused;\n\n  const clamp = (v: number) => {\n    let n = v;\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 setVal = (v: number) => {\n    const c = clamp(v);\n    setValue(c);\n    onChange?.(c);\n  };\n\n  const increment = () => {\n    const next = clamp(value + step);\n    if (next !== value) {\n      if (sound) tick();\n      setVal(next);\n    }\n  };\n\n  const decrement = () => {\n    const next = clamp(value - step);\n    if (next !== value) {\n      if (sound) tick();\n      setVal(next);\n    }\n  };\n\n  return (\n    <>\n      <style dangerouslySetInnerHTML={{ __html: CSS }} />\n      <motion.div\n        className=\"ni\"\n        initial={{ opacity: 0, y: 8 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ duration: 0.35, ease: [0.25, 0.1, 0.25, 1] }}\n        style={{\n          display: \"inline-flex\",\n          flexDirection: \"column\",\n          gap: 6,\n          ...style,\n        }}\n      >\n        {/* glass field */}\n        <div\n          style={{\n            position: \"relative\",\n            display: \"flex\",\n            alignItems: \"stretch\",\n            borderRadius: 12,\n            background: \"var(--ni-glass)\",\n            border: `1px solid ${focused ? \"var(--ni-focus)\" : \"var(--ni-border)\"}`,\n            boxShadow: \"var(--ni-shadow)\",\n            backdropFilter: \"blur(24px)\",\n            WebkitBackdropFilter: \"blur(24px)\",\n            transition: \"border-color 0.2s\",\n            overflow: \"hidden\",\n          }}\n          onClick={() => inputRef.current?.focus()}\n        >\n          {/* input area */}\n          <div style={{ flex: 1, position: \"relative\", padding: \"0 16px\" }}>\n            {/* floating label */}\n            <span\n              style={{\n                position: \"absolute\",\n                left: 16,\n                top: hasValue ? 8 : \"50%\",\n                transform: hasValue ? \"none\" : \"translateY(-50%)\",\n                fontSize: hasValue ? 10 : 14,\n                fontWeight: 500,\n                color: \"var(--ni-dim)\",\n                transition: \"all 0.2s\",\n                pointerEvents: \"none\",\n                userSelect: \"none\",\n              }}\n            >\n              {label}\n            </span>\n            <input\n              ref={inputRef}\n              type=\"text\"\n              inputMode=\"numeric\"\n              value={value === 0 && !focused ? \"\" : value}\n              onChange={(e) => {\n                const raw = e.target.value;\n                if (raw === \"\" || raw === \"-\") {\n                  setValue(0);\n                  return;\n                }\n                const n = Number(raw);\n                if (!isNaN(n)) setVal(n);\n              }}\n              onFocus={() => setFocused(true)}\n              onBlur={() => setFocused(false)}\n              style={{\n                width: \"100%\",\n                height: 52,\n                paddingTop: hasValue ? 18 : 0,\n                background: \"transparent\",\n                border: \"none\",\n                outline: \"none\",\n                fontSize: 15,\n                fontWeight: 500,\n                fontVariantNumeric: \"tabular-nums\",\n                color: \"var(--ni-hi)\",\n              }}\n            />\n          </div>\n\n          {/* separator */}\n          <div\n            style={{\n              width: 1,\n              alignSelf: \"stretch\",\n              background: \"var(--ni-sep)\",\n            }}\n          />\n\n          {/* steppers */}\n          <div\n            style={{\n              display: \"flex\",\n              flexDirection: \"column\",\n              width: 36,\n            }}\n          >\n            <StepBtn dir=\"up\" onClick={increment} />\n            <div style={{ height: 1, background: \"var(--ni-sep)\" }} />\n            <StepBtn dir=\"down\" onClick={decrement} />\n          </div>\n        </div>\n\n        {hint && (\n          <span\n            style={{\n              fontSize: 11,\n              color: \"var(--ni-dim)\",\n              paddingLeft: 4,\n            }}\n          >\n            {hint}\n          </span>\n        )}\n      </motion.div>\n    </>\n  );\n}\n\n/* ── step button ── */\nfunction StepBtn({\n  dir,\n  onClick,\n}: {\n  dir: \"up\" | \"down\";\n  onClick: () => void;\n}) {\n  return (\n    <motion.button\n      type=\"button\"\n      onClick={onClick}\n      whileTap={{ scale: 0.85 }}\n      style={{\n        flex: 1,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        background: \"var(--ni-btn)\",\n        border: \"none\",\n        cursor: \"pointer\",\n        color: \"var(--ni-mid)\",\n        transition: \"background 0.15s\",\n      }}\n      onMouseEnter={(e) => {\n        (e.currentTarget as HTMLElement).style.background = \"var(--ni-btn-h)\";\n      }}\n      onMouseLeave={(e) => {\n        (e.currentTarget as HTMLElement).style.background = \"var(--ni-btn)\";\n      }}\n    >\n      <svg\n        width={12}\n        height={12}\n        viewBox=\"0 0 16 16\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={2}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      >\n        {dir === \"up\" ? <path d=\"M4 10l4-4 4 4\" /> : <path d=\"M4 6l4 4 4-4\" />}\n      </svg>\n    </motion.button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ruixen/correct-number-input.tsx"
    }
  ]
}