-
Enter code
input-otp.tsx
globals.css
prompt.md
Collapse
Reset
Copy Code
Download
Run
1
'use client';
2
3
import React, { useState, useRef, useEffect } from 'react';
4
import { motion, useAnimation } from 'framer-motion';
5
import { cn } from '@/lib/utils';
6
7
interface InputOTPProps {
8
validCode?: string;
9
onSuccess?: () => void;
10
onError?: () => void;
11
className?: string;
12
}
13
14
export function InputOTP({ validCode = "800813", onSuccess, onError, className }: InputOTPProps) {
15
const [code, setCode] = useState<string[]>(Array(6).fill(''));
16
const [status, setStatus] = useState<'idle' | 'processing' | 'success' | 'error'>('idle');
17
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
18
const containerControls = useAnimation();
19
20
useEffect(() => {
21
// Auto-focus first input on mount
22
inputRefs.current[0]?.focus();
23
}, []);
24
25
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
26
if (status !== 'idle' && status !== 'error') return;
27
28
if (e.key === 'Backspace') {
29
e.preventDefault();
30
const newCode = [...code];
31
if (newCode[index]) {
32
newCode[index] = '';
33
} else if (index > 0) {
34
newCode[index - 1] = '';
35
inputRefs.current[index - 1]?.focus();
36
}
37
setCode(newCode);
38
if (status === 'error') setStatus('idle');
39
}
40
};
41
42
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
43
if (status !== 'idle' && status !== 'error') return;
44
if (status === 'error') setStatus('idle');
45
46
const value = e.target.value.replace(/[^0-9]/g, '');
47
if (!value) return;
48
49
// Handle pasting multiple characters
50
if (value.length > 1) {
51
const newCode = [...code];
52
const chars = value.split('').slice(0, 6 - index);
53
chars.forEach((char, i) => {
54
newCode[index + i] = char;
55
});
56
setCode(newCode);
57
const nextFocus = Math.min(index + chars.length, 5);
58
inputRefs.current[nextFocus]?.focus();
59
checkCode(newCode);
60
return;
61
}
62
63
const newCode = [...code];
64
newCode[index] = value;
65
setCode(newCode);
66
67
if (index < 5) {
68
inputRefs.current[index + 1]?.focus();
69
} else {
70
checkCode(newCode);
71
}
72
};
73
74
const checkCode = async (currentCode: string[]) => {
75
const fullCode = currentCode.join('');
76
if (fullCode.length === 6) {
77
setStatus('processing');
78
// Simulate network request
79
await new Promise(r => setTimeout(r, 1500));
80
81
if (fullCode === validCode) {
82
setStatus('success');
83
onSuccess?.();
84
setTimeout(() => {
85
setCode(Array(6).fill(''));
86
setStatus('idle');
87
inputRefs.current[0]?.focus();
88
}, 2000);
89
} else {
90
setStatus('error');
91
containerControls.start({
92
x: [-8, 8, -6, 6, -4, 4, 0],
93
transition: { duration: 0.4 }
94
});
95
setTimeout(() => {
96
setCode(Array(6).fill(''));
97
setStatus('idle');
98
inputRefs.current[0]?.focus();
99
}, 1500);
100
onError?.();
101
}
102
}
103
};
104
105
return (
106
<div className={cn("flex flex-col items-center gap-6", className)}>
107
<motion.div
108
animate={containerControls}
109
className="flex items-center gap-3 relative"
110
>
111
{/* Processing Shimmer Effect */}
112
{status === 'processing' && (
113
<motion.div
114
className="absolute inset-0 z-10 pointer-events-none rounded-md overflow-hidden"
115
initial={{ opacity: 0 }}
116
animate={{ opacity: 1 }}
117
>
118
<motion.div
119
className="w-[200%] h-full bg-gradient-to-r from-transparent via-white/60 dark:via-white/20 to-transparent skew-x-12"
120
initial={{ x: '-150%' }}
121
animate={{ x: '100%' }}
122
transition={{ repeat: Infinity, duration: 1.2, ease: "linear" }}
123
/>
124
</motion.div>
125
)}
126
127
{[0, 1, 2].map((index) => (
128
<OTPInput
129
key={index}
130
index={index}
131
value={code[index]}
132
status={status}
133
autoFocus={index === 0}
134
onChange={(e) => handleChange(e, index)}
135
onKeyDown={(e) => handleKeyDown(e, index)}
136
ref={(el) => { inputRefs.current[index] = el; }}
137
/>
138
))}
139
140
<div className="w-3 flex justify-center text-zinc-300 dark:text-zinc-700 font-bold">
141
-
142
</div>
143
144
{[3, 4, 5].map((index) => (
145
<OTPInput
146
key={index}
147
index={index}
148
value={code[index]}
149
status={status}
150
onChange={(e) => handleChange(e, index)}
151
onKeyDown={(e) => handleKeyDown(e, index)}
152
ref={(el) => (inputRefs.current[index] = el)}
153
/>
154
))}
155
</motion.div>
156
157
{/* Dynamic Feedback Message */}
158
<div className="h-4 flex items-center justify-center font-matter text-[13px] font-medium transition-all duration-300">
159
{status === 'success' && <span className="text-green-600 dark:text-green-400">Successfully verified!</span>}
160
{status === 'error' && <span className="text-red-500 dark:text-red-400">Invalid code. Use {validCode}.</span>}
161
{status === 'processing' && <span className="text-zinc-400 dark:text-zinc-500">Verifying code...</span>}
162
{status === 'idle' && <span className="text-zinc-400 dark:text-zinc-500 opacity-0">Enter code</span>}
163
</div>
164
</div>
165
);
166
}
167
168
interface OTPInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
169
index: number;
170
status: 'idle' | 'processing' | 'success' | 'error';
171
}
172
173
const OTPInput = React.forwardRef<HTMLInputElement, OTPInputProps>(
174
({ index, status, className, ...props }, ref) => {
175
return (
176
<motion.div
177
custom={index}
178
initial={{ y: 0 }}
179
animate={
180
status === 'success'
181
? { y: [0, -12, 0] }
182
: { y: 0 }
183
}
184
transition={{
185
delay: status === 'success' ? index * 0.08 : 0,
186
duration: 0.5,
187
ease: [0.23, 1, 0.32, 1]
188
}}
189
className="relative"
190
>
191
<input
192
ref={ref}
193
type="text"
194
inputMode="numeric"
195
autoComplete="one-time-code"
196
maxLength={1}
197
disabled={status === 'processing' || status === 'success'}
198
className={cn(
199
"w-12 h-14 md:w-14 md:h-16 text-center text-xl md:text-2xl font-matter font-semibold text-zinc-950 dark:text-zinc-50 rounded-md outline-none transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]",
200
"bg-zinc-100 dark:bg-zinc-900 border border-transparent shadow-inner",
201
"focus:bg-white dark:focus:bg-black focus:shadow-[0_4px_16px_rgba(0,0,0,0.05)] dark:focus:shadow-[0_4px_16px_rgba(255,255,255,0.02)] focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-black focus:ring-zinc-950 dark:focus:ring-zinc-50",
202
status === 'success' && "border-green-500/50 dark:border-green-400/50 bg-green-50 dark:bg-green-950/20 text-green-700 dark:text-green-400 shadow-none ring-0",
203
status === 'error' && "border-red-500/50 dark:border-red-400/50 bg-red-50 dark:bg-red-950/20 text-red-700 dark:text-red-400 focus:ring-red-500 dark:focus:ring-red-400 shadow-none",
204
status === 'processing' && "opacity-60",
205
className
206
)}
207
{...props}
208
/>
209
</motion.div>
210
);
211
}
212
);
213
214
OTPInput.displayName = 'OTPInput';

