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...

default.tsx
"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;