{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "verification-input", "type": "registry:ui", "title": "Verification Input", "description": "iOS 26 liquid-glass verification code input — dual-theme cells with spring-animated digit entry and haptic sound.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/verification-input.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, AnimatePresence } 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.04;\n s.connect(g).connect(_a.destination);\n s.start();\n};\n\n/* ── theme ── */\nconst CSS = `\n.vi{\n --vi-dim:rgba(0,0,0,0.42);\n --vi-mid:rgba(0,0,0,0.55);\n --vi-hi:rgba(0,0,0,0.88);\n --vi-cell:rgba(0,0,0,0.02);\n --vi-cell-border:rgba(0,0,0,0.06);\n --vi-cell-active:rgba(0,0,0,0.12);\n --vi-cursor:rgba(0,0,0,0.2);\n --vi-filled:rgba(0,0,0,0.6);\n --vi-complete:rgba(0,0,0,0.85)\n}\n.dark .vi,[data-theme=\"dark\"] .vi{\n --vi-dim:rgba(255,255,255,0.28);\n --vi-mid:rgba(255,255,255,0.5);\n --vi-hi:rgba(255,255,255,0.88);\n --vi-cell:rgba(255,255,255,0.02);\n --vi-cell-border:rgba(255,255,255,0.06);\n --vi-cell-active:rgba(255,255,255,0.15);\n --vi-cursor:rgba(255,255,255,0.2);\n --vi-filled:rgba(255,255,255,0.6);\n --vi-complete:rgba(255,255,255,0.85)\n}`;\n\nconst CELL = 46;\nconst CELL_H = 56;\nconst GAP = 8;\nconst GROUP_GAP = 16;\nconst MONO =\n \"ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace\";\n\n/* ── types ── */\nexport interface VerificationInputProps {\n length?: number;\n onComplete?: (code: string) => void;\n onResend?: () => void;\n sound?: boolean;\n style?: React.CSSProperties;\n}\n\n/* ── component ── */\nexport function VerificationInput({\n length = 6,\n onComplete,\n onResend,\n sound = true,\n style,\n}: VerificationInputProps) {\n const [value, setValue] = React.useState(\"\");\n const [focused, setFocused] = React.useState(false);\n const inputRef = React.useRef(null);\n\n const digits = value.split(\"\");\n const activeIndex = value.length;\n const completed = value.length >= length;\n const group = Math.floor(length / 2);\n\n React.useEffect(() => {\n const t = setTimeout(() => inputRef.current?.focus(), 80);\n return () => clearTimeout(t);\n }, []);\n\n const handleChange = React.useCallback(\n (e: React.ChangeEvent) => {\n const raw = e.target.value.replace(/\\D/g, \"\").slice(0, length);\n if (raw.length > value.length && sound) tick();\n setValue(raw);\n if (raw.length >= length) onComplete?.(raw);\n },\n [length, value.length, sound, onComplete],\n );\n\n const handleKeyDown = React.useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === \"Backspace\" && sound && value.length > 0) tick();\n },\n [sound, value.length],\n );\n\n return (\n <>\n