"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { format } from "date-fns"
import { useTheme } from "next-themes"
import {
User,
Mail,
Calendar,
MapPin,
Lock,
Eye,
EyeOff,
CheckCircle2,
ArrowRight,
ArrowLeft,
Moon,
Sun,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Calendar as CalendarComponent } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { cn } from "@/lib/utils"
// Form schemas for each step
const step1Schema = z.object({
firstName: z.string().min(2, "First name must be at least 2 characters"),
lastName: z.string().min(2, "Last name must be at least 2 characters"),
email: z.string().email("Please enter a valid email address"),
})
const step2Schema = z.object({
dob: z.date({
required_error: "Please select a date of birth",
}),
location: z.string().min(2, "Please enter your location"),
occupation: z.string().min(2, "Please enter your occupation"),
})
const step3Schema = z
.object({
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/[0-9]/, "Password must contain at least one number"),
confirmPassword: z.string(),
notifications: z.enum(["all", "important", "none"], {
required_error: "Please select a notification preference",
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
})
// Combined schema type
type FormData = z.infer<typeof step1Schema> & z.infer<typeof step2Schema> & z.infer<typeof step3Schema>
export default function MultiStepRegisterForm() {
const [step, setStep] = useState(1)
const [formData, setFormData] = useState<Partial<FormData>>({})
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [isComplete, setIsComplete] = useState(false)
const { theme, setTheme } = useTheme()
// Form setup for current step
const {
register,
handleSubmit,
formState: { errors, isValid },
setValue,
watch,
trigger,
} = useForm<Partial<FormData>>({
resolver: zodResolver(step === 1 ? step1Schema : step === 2 ? step2Schema : step3Schema),
mode: "onChange",
defaultValues: formData,
})
// Handle form submission for each step
const onSubmit = async (data: Partial<FormData>) => {
const updatedData = { ...formData, ...data }
setFormData(updatedData)
if (step < 3) {
setStep(step + 1)
} else {
// Final submission
console.log("Form submitted:", updatedData)
setIsComplete(true)
}
}
// Go back to previous step
const handleBack = () => {
if (step > 1) {
setStep(step - 1)
}
}
// Reset the form
const handleReset = () => {
setFormData({})
setStep(1)
setIsComplete(false)
}
// Toggle theme
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark")
}
// Animation variants
const formVariants = {
hidden: (direction: number) => ({
x: direction > 0 ? 200 : -200,
opacity: 0,
}),
visible: {
x: 0,
opacity: 1,
transition: {
duration: 0.3,
type: "spring",
stiffness: 300,
damping: 30,
},
},
exit: (direction: number) => ({
x: direction > 0 ? -200 : 200,
opacity: 0,
transition: {
duration: 0.3,
},
}),
}
// Direction for animations
const [direction, setDirection] = useState(0)
// Handle next step with direction
const handleNext = async () => {
const isStepValid = await trigger()
if (isStepValid) {
setDirection(1)
handleSubmit(onSubmit)()
}
}
// Handle previous step with direction
const handlePrevious = () => {
setDirection(-1)
handleBack()
}
return (
<div className="w-full max-w-md mx-auto p-6 rounded-xl shadow-lg transition-colors duration-300 bg-gradient-to-br from-white to-slate-100 dark:from-slate-900 dark:to-slate-800">
{/* Header with theme toggle */}
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-purple-600 dark:from-blue-400 dark:to-purple-500">
{isComplete ? "Registration Complete" : `Create Account: Step ${step}/3`}
</h2>
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="rounded-full hover:bg-slate-200 dark:hover:bg-slate-700"
>
<motion.div initial={false} animate={{ rotate: theme === "dark" ? 180 : 0 }} transition={{ duration: 0.5 }}>
{theme === "dark" ? (
<Sun className="h-5 w-5 text-yellow-400" />
) : (
<Moon className="h-5 w-5 text-slate-700" />
)}
</motion.div>
</Button>
</div>
{/* Progress bar */}
{!isComplete && (
<div className="mb-8">
<div className="relative h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
<motion.div
className="absolute top-0 left-0 h-full bg-gradient-to-r from-blue-500 to-purple-600 dark:from-blue-400 dark:to-purple-500"
initial={{ width: "0%" }}
animate={{ width: `${(step / 3) * 100}%` }}
transition={{ duration: 0.5, ease: "easeInOut" }}
/>
</div>
<div className="flex justify-between mt-2">
{[1, 2, 3].map((stepNumber) => (
<motion.div
key={stepNumber}
className={cn(
"flex flex-col items-center",
step >= stepNumber ? "text-blue-600 dark:text-blue-400" : "text-slate-400 dark:text-slate-500",
)}
animate={{
scale: step === stepNumber ? 1.1 : 1,
}}
transition={{ duration: 0.2 }}
>
<div
className={cn(
"w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors",
step > stepNumber
? "bg-blue-600 text-white dark:bg-blue-500"
: step === stepNumber
? "bg-white dark:bg-slate-800 border-2 border-blue-500 dark:border-blue-400"
: "bg-slate-200 dark:bg-slate-700 text-slate-500 dark:text-slate-400",
)}
>
{step > stepNumber ? <CheckCircle2 className="h-5 w-5" /> : stepNumber}
</div>
<span className="text-xs mt-1 font-medium">
{stepNumber === 1 ? "Info" : stepNumber === 2 ? "Details" : "Account"}
</span>
</motion.div>
))}
</div>
</div>
)}
{/* Form content */}
<div className="relative overflow-hidden">
<AnimatePresence custom={direction} mode="wait">
{isComplete ? (
<motion.div
key="complete"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, type: "spring" }}
className="flex flex-col items-center text-center"
>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="w-20 h-20 rounded-full bg-gradient-to-r from-green-400 to-emerald-500 flex items-center justify-center mb-4"
>
<CheckCircle2 className="h-10 w-10 text-white" />
</motion.div>
<h3 className="text-xl font-bold mb-2">Welcome aboard!</h3>
<p className="text-slate-600 dark:text-slate-300 mb-6">Your account has been successfully created.</p>
<Button
onClick={handleReset}
className="bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white"
>
Create Another Account
</Button>
</motion.div>
) : step === 1 ? (
<motion.form
key="step1"
custom={direction}
variants={formVariants}
initial="hidden"
animate="visible"
exit="exit"
className="space-y-4"
onSubmit={handleSubmit(onSubmit)}
>
<div className="space-y-2">
<Label htmlFor="firstName" className="text-slate-700 dark:text-slate-200">
First Name
</Label>
<div className="relative">
<Input
id="firstName"
placeholder="John"
className={cn(
"pl-10 bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-all",
errors.firstName &&
"border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500",
)}
{...register("firstName")}
/>
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
</div>
{errors.firstName && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-sm text-red-500 mt-1"
>
{errors.firstName.message}
</motion.p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="lastName" className="text-slate-700 dark:text-slate-200">
Last Name
</Label>
<div className="relative">
<Input
id="lastName"
placeholder="Doe"
className={cn(
"pl-10 bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-all",
errors.lastName &&
"border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500",
)}
{...register("lastName")}
/>
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
</div>
{errors.lastName && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-sm text-red-500 mt-1"
>
{errors.lastName.message}
</motion.p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="email" className="text-slate-700 dark:text-slate-200">
Email Address
</Label>
<div className="relative">
<Input
id="email"
type="email"
placeholder="john.doe@example.com"
className={cn(
"pl-10 bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-all",
errors.email && "border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500",
)}
{...register("email")}
/>
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
</div>
{errors.email && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-sm text-red-500 mt-1"
>
{errors.email.message}
</motion.p>
)}
</div>
<div className="pt-4">
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Button
type="button"
onClick={handleNext}
disabled={!isValid}
className={cn(
"w-full transition-all",
isValid
? "bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white"
: "bg-slate-300 dark:bg-slate-700 text-slate-500 dark:text-slate-400 cursor-not-allowed",
)}
>
<span>Continue</span>
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</motion.div>
</div>
</motion.form>
) : step === 2 ? (
<motion.form
key="step2"
custom={direction}
variants={formVariants}
initial="hidden"
animate="visible"
exit="exit"
className="space-y-4"
onSubmit={handleSubmit(onSubmit)}
>
<div className="space-y-2">
<Label htmlFor="dob" className="text-slate-700 dark:text-slate-200">
Date of Birth
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full justify-start text-left font-normal pl-10 relative bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600",
errors.dob && "border-red-500 dark:border-red-500",
)}
>
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
{formData.dob ? (
format(formData.dob, "PPP")
) : (
<span className="text-slate-500 dark:text-slate-400">Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<CalendarComponent
mode="single"
selected={formData.dob}
onSelect={(date) => {
if (date) {
setValue("dob", date, { shouldValidate: true })
setFormData({ ...formData, dob: date })
}
}}
disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
initialFocus
/>
</PopoverContent>
</Popover>
{errors.dob && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-sm text-red-500 mt-1"
>
{errors.dob.message as string}
</motion.p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="location" className="text-slate-700 dark:text-slate-200">
Location
</Label>
<div className="relative">
<Input
id="location"
placeholder="New York, USA"
className={cn(
"pl-10 bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-all",
errors.location &&
"border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500",
)}
{...register("location")}
/>
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
</div>
{errors.location && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-sm text-red-500 mt-1"
>
{errors.location.message}
</motion.p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="occupation" className="text-slate-700 dark:text-slate-200">
Occupation
</Label>
<div className="relative">
<Input
id="occupation"
placeholder="Software Developer"
className={cn(
"pl-10 bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-all",
errors.occupation &&
"border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500",
)}
{...register("occupation")}
/>
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
</div>
{errors.occupation && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-sm text-red-500 mt-1"
>
{errors.occupation.message}
</motion.p>
)}
</div>
<div className="flex gap-2 pt-4">
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} className="flex-1">
<Button
type="button"
onClick={handlePrevious}
variant="outline"
className="w-full border-slate-300 dark:border-slate-600"
>
<ArrowLeft className="mr-2 h-4 w-4" />
<span>Back</span>
</Button>
</motion.div>
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} className="flex-1">
<Button
type="button"
onClick={handleNext}
disabled={!isValid}
className={cn(
"w-full transition-all",
isValid
? "bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white"
: "bg-slate-300 dark:bg-slate-700 text-slate-500 dark:text-slate-400 cursor-not-allowed",
)}
>
<span>Continue</span>
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</motion.div>
</div>
</motion.form>
) : (
<motion.form
key="step3"
custom={direction}
variants={formVariants}
initial="hidden"
animate="visible"
exit="exit"
className="space-y-4"
onSubmit={handleSubmit(onSubmit)}
>
<div className="space-y-2">
<Label htmlFor="password" className="text-slate-700 dark:text-slate-200">
Password
</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="••••••••"
className={cn(
"pl-10 pr-10 bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-all",
errors.password &&
"border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500",
)}
{...register("password")}
/>
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-8 w-8 p-0"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-slate-400" />
) : (
<Eye className="h-5 w-5 text-slate-400" />
)}
</Button>
</div>
{errors.password && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-sm text-red-500 mt-1"
>
{errors.password.message}
</motion.p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword" className="text-slate-700 dark:text-slate-200">
Confirm Password
</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
placeholder="••••••••"
className={cn(
"pl-10 pr-10 bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-all",
errors.confirmPassword &&
"border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500",
)}
{...register("confirmPassword")}
/>
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-8 w-8 p-0"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? (
<EyeOff className="h-5 w-5 text-slate-400" />
) : (
<Eye className="h-5 w-5 text-slate-400" />
)}
</Button>
</div>
{errors.confirmPassword && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-sm text-red-500 mt-1"
>
{errors.confirmPassword.message}
</motion.p>
)}
</div>
<div className="space-y-3">
<Label className="text-slate-700 dark:text-slate-200">Notification Preferences</Label>
<RadioGroup
defaultValue={formData.notifications || "all"}
onValueChange={(value) => {
setValue("notifications", value as "all" | "important" | "none", { shouldValidate: true })
setFormData({ ...formData, notifications: value as "all" | "important" | "none" })
}}
className="space-y-2"
>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="all"
id="all"
className="border-slate-300 dark:border-slate-600 text-blue-500"
/>
<Label htmlFor="all" className="text-slate-700 dark:text-slate-200 cursor-pointer">
All notifications
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="important"
id="important"
className="border-slate-300 dark:border-slate-600 text-blue-500"
/>
<Label htmlFor="important" className="text-slate-700 dark:text-slate-200 cursor-pointer">
Important only
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="none"
id="none"
className="border-slate-300 dark:border-slate-600 text-blue-500"
/>
<Label htmlFor="none" className="text-slate-700 dark:text-slate-200 cursor-pointer">
No notifications
</Label>
</div>
</RadioGroup>
{errors.notifications && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-sm text-red-500 mt-1"
>
{errors.notifications.message}
</motion.p>
)}
</div>
<div className="flex gap-2 pt-4">
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} className="flex-1">
<Button
type="button"
onClick={handlePrevious}
variant="outline"
className="w-full border-slate-300 dark:border-slate-600"
>
<ArrowLeft className="mr-2 h-4 w-4" />
<span>Back</span>
</Button>
</motion.div>
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} className="flex-1">
<Button
type="submit"
disabled={!isValid}
className={cn(
"w-full transition-all",
isValid
? "bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white"
: "bg-slate-300 dark:bg-slate-700 text-slate-500 dark:text-slate-400 cursor-not-allowed",
)}
>
<span>Complete Registration</span>
<CheckCircle2 className="ml-2 h-4 w-4" />
</Button>
</motion.div>
</div>
</motion.form>
)}
</AnimatePresence>
</div>
{/* Password strength indicator (only on step 3) */}
{step === 3 && !isComplete && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mt-6"
>
<Label className="text-sm text-slate-600 dark:text-slate-400 mb-2 block">Password Strength</Label>
<div className="h-2 w-full bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
{watch("password") && (
<motion.div
className={cn(
"h-full",
watch("password")?.length < 6
? "bg-red-500"
: watch("password")?.length < 8
? "bg-yellow-500"
: "bg-green-500",
)}
initial={{ width: 0 }}
animate={{
width: watch("password") ? Math.min(100, (watch("password")?.length || 0) * 10) + "%" : "0%",
}}
transition={{ duration: 0.5 }}
/>
)}
</div>
<div className="flex justify-between mt-1 text-xs">
<span className="text-slate-500 dark:text-slate-400">Weak</span>
<span className="text-slate-500 dark:text-slate-400">Strong</span>
</div>
</motion.div>
)}
</div>
)
}