Multi-Step Registration Form

PreviousNext

A stylish 3-step registration form with animations and dark mode support.

Docs
react-marketcomponent

Preview

Loading preview…
multi-step-register-form.tsx
"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>
  )
}

Installation

npx shadcn@latest add @react-market/multi-step-register-form

Usage

import { MultiStepRegisterForm } from "@/components/multi-step-register-form"
<MultiStepRegisterForm />