Form
A sleek 3-step email & OTP form with animated loading, inline validation, and real-time feedback for a smooth signup flow inspired by Devouring details login.
Loading demo...
"use client";
import { cn } from "@/lib/utils";
import { ArrowRight02Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
type Step = "email" | "otp" | "done";
const Form = () => {
const [step, setStep] = useState<Step>("email");
const [email, setEmail] = useState("");
const [otp, setOtp] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [generatedOtp, setGeneratedOtp] = useState<string | null>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const isValidEmail = (value: string) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
const generateOtp = () => {
const code = Math.floor(100000 + Math.random() * 900000).toString();
setGeneratedOtp(code);
return code;
};
const runLoadingAnimation = async () => {
setIsLoading(true);
await new Promise((res) => setTimeout(res, 3000));
setIsLoading(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (step === "email") {
if (!isValidEmail(email)) return;
await runLoadingAnimation();
generateOtp();
setStep("otp");
return;
}
if (step === "otp") {
if (otp !== generatedOtp) return;
await runLoadingAnimation();
setStep("done");
}
};
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);
return (
<div className="size-full flex items-center justify-center">
<form
onSubmit={handleSubmit}
className="gap-4 max-w-lg mx-auto w-full flex flex-col items-center relative overflow-hidden transition duration-300"
>
<div className="flex items-center w-full px-1">
{step === "email" && (
<input
type="email"
placeholder="hello@bossadizenith.me"
disabled={isLoading}
className="border-none outline-none w-full"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
)}
{step === "otp" && (
<input
type="text"
disabled={isLoading}
placeholder="Enter OTP"
className="border-none outline-none w-full tracking-widest"
value={otp}
onChange={(e) => setOtp(e.target.value)}
/>
)}
{step === "done" && (
<p className="font-medium text-blue-500 text-center w-full">
Thanks, you’re in ✨
</p>
)}
{step !== "done" && (
<Button
size="icon"
variant="ghost"
type="submit"
disabled={isLoading || email.length < 0}
>
<HugeiconsIcon icon={ArrowRight02Icon} />
</Button>
)}
</div>
<div
className={cn(
"h-px bg-border w-full relative transition-all duration-300",
step === "done" && "bg-blue-500",
)}
>
<AnimatePresence>
{isLoading && (
<motion.div
className="absolute left-0 h-full w-1/3
bg-linear-to-r from-transparent via-blue-500 to-transparent"
initial={{ x: "0%" }}
animate={{ x: ["0%", "200%"], opacity: 1 }}
transition={{
duration: 0.5,
repeat: Infinity,
repeatType: "mirror",
}}
/>
)}
</AnimatePresence>
</div>
</form>
<AnimatePresence>
{generatedOtp && step === "otp" && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="mt-4 text-sm text-muted-foreground absolute bottom-20"
>
OTP sent: <span className="font-mono">{generatedOtp}</span>
</motion.p>
)}
</AnimatePresence>
</div>
);
};
export default Form;