diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx
new file mode 100644
index 0000000..9d6cf24
--- /dev/null
+++ b/app/(auth)/signup/page.tsx
@@ -0,0 +1,10 @@
+import { Suspense } from "react"
+import SignupWizard from "@/components/auth/signup-wizard"
+
+export default function SignupPage() {
+ return (
+
+
+
+ )
+}
diff --git a/components/auth/otp-verification-step.tsx b/components/auth/otp-verification-step.tsx
new file mode 100644
index 0000000..cc6fa6c
--- /dev/null
+++ b/components/auth/otp-verification-step.tsx
@@ -0,0 +1,146 @@
+"use client"
+
+import { useState, useEffect, useCallback } from "react"
+import { ArrowLeft, Loader2, Mail } from "lucide-react"
+import { authClient } from "@/lib/auth-client"
+import OtpInput from "./otp-input"
+
+interface OtpVerificationStepProps {
+ email: string
+ onSuccess: () => void
+ onBack: () => void
+}
+
+export default function OtpVerificationStep({
+ email,
+ onSuccess,
+ onBack,
+}: OtpVerificationStepProps) {
+ const [otp, setOtp] = useState("")
+ const [error, setError] = useState("")
+ const [isLoading, setIsLoading] = useState(false)
+ const [resendTimer, setResendTimer] = useState(300) // 5 minutes
+ const [canResend, setCanResend] = useState(false)
+
+ // Countdown timer
+ useEffect(() => {
+ if (resendTimer <= 0) {
+ setCanResend(true)
+ return
+ }
+ const interval = setInterval(() => {
+ setResendTimer((prev) => prev - 1)
+ }, 1000)
+ return () => clearInterval(interval)
+ }, [resendTimer])
+
+ const formatTime = (seconds: number) => {
+ const mins = Math.floor(seconds / 60)
+ const secs = seconds % 60
+ return `${mins}:${secs.toString().padStart(2, "0")}`
+ }
+
+ const handleVerify = useCallback(async () => {
+ if (otp.length !== 6) return
+
+ setIsLoading(true)
+ setError("")
+
+ const { error } = await authClient.emailOtp.verifyEmail({
+ email,
+ otp,
+ })
+
+ setIsLoading(false)
+
+ if (error) {
+ setError(
+ error.code === "TOO_MANY_ATTEMPTS"
+ ? "Too many attempts. Please request a new code."
+ : "Invalid code. Please try again.",
+ )
+ return
+ }
+
+ onSuccess()
+ }, [otp, email, onSuccess])
+
+ // Auto-submit when all digits entered
+ useEffect(() => {
+ if (otp.length === 6) {
+ handleVerify()
+ }
+ }, [otp, handleVerify])
+
+ const handleResend = async () => {
+ setError("")
+ await authClient.emailOtp.sendVerificationOtp({
+ email,
+ type: "email-verification",
+ })
+ setResendTimer(300)
+ setCanResend(false)
+ }
+
+ return (
+
+
+
+
+ Verify your email
+
+
+ We sent a 6-digit code to{" "}
+ {email}
+
+
+
+
+
+
+
+
+
+ {canResend ? (
+
+ ) : (
+
+ Resend code in{" "}
+
+ {formatTime(resendTimer)}
+
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/components/auth/passkey-setup-step.tsx b/components/auth/passkey-setup-step.tsx
new file mode 100644
index 0000000..6ef47d1
--- /dev/null
+++ b/components/auth/passkey-setup-step.tsx
@@ -0,0 +1,115 @@
+"use client"
+
+import { useState } from "react"
+import { KeyRound, Check, Loader2, Fingerprint } from "lucide-react"
+import { authClient } from "@/lib/auth-client"
+
+interface PasskeySetupStepProps {
+ onSuccess: () => void
+ onSkip: () => void
+}
+
+export default function PasskeySetupStep({
+ onSuccess,
+ onSkip,
+}: PasskeySetupStepProps) {
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState("")
+
+ const isPasskeySupported =
+ typeof window !== "undefined" &&
+ "PublicKeyCredential" in window
+
+ const handleAddPasskey = async () => {
+ setIsLoading(true)
+ setError("")
+
+ const { error } = await authClient.passkey.addPasskey({
+ name: "Primary passkey",
+ })
+
+ setIsLoading(false)
+
+ if (error) {
+ setError(
+ error.message ||
+ "Failed to set up passkey. You can try again later.",
+ )
+ return
+ }
+
+ onSuccess()
+ }
+
+ return (
+
+
+
+
+ Set up a passkey
+
+
+ Sign in faster with biometrics or your device's security
+ key. No password needed.
+
+
+
+ {/* Benefits list */}
+
+
+
+ Faster sign-in with fingerprint or face
+
+
+
+ More secure than passwords
+
+
+
+ Works across your devices
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {isPasskeySupported ? (
+ <>
+
+
+ >
+ ) : (
+
+
+ Passkeys are not supported on this device.
+
+
+
+ )}
+
+ )
+}
diff --git a/components/auth/signup-form-step.tsx b/components/auth/signup-form-step.tsx
new file mode 100644
index 0000000..08c6358
--- /dev/null
+++ b/components/auth/signup-form-step.tsx
@@ -0,0 +1,152 @@
+"use client"
+
+import { useState } from "react"
+import { Loader2 } from "lucide-react"
+import { authClient } from "@/lib/auth-client"
+import { signupSchema, type SignupInput } from "@/lib/auth/validation"
+import SocialButtons from "./social-buttons"
+import PasswordStrengthMeter from "./password-strength"
+import Link from "next/link"
+
+interface SignupFormStepProps {
+ onSuccess: (email: string) => void
+}
+
+export default function SignupFormStep({ onSuccess }: SignupFormStepProps) {
+ const [name, setName] = useState("")
+ const [email, setEmail] = useState("")
+ const [password, setPassword] = useState("")
+ const [errors, setErrors] = useState>({})
+ const [serverError, setServerError] = useState("")
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setErrors({})
+ setServerError("")
+
+ const result = signupSchema.safeParse({ name, email, password })
+ if (!result.success) {
+ const fieldErrors: Record = {}
+ result.error.issues.forEach((issue) => {
+ const field = issue.path[0] as string
+ fieldErrors[field] = issue.message
+ })
+ setErrors(fieldErrors)
+ return
+ }
+
+ setIsLoading(true)
+ const { error } = await authClient.signUp.email({
+ name,
+ email,
+ password,
+ })
+ setIsLoading(false)
+
+ if (error) {
+ setServerError(
+ error.message || "Something went wrong. Please try again.",
+ )
+ return
+ }
+
+ onSuccess(email)
+ }
+
+ return (
+
+ )
+}
diff --git a/components/auth/signup-wizard.tsx b/components/auth/signup-wizard.tsx
new file mode 100644
index 0000000..46e0bad
--- /dev/null
+++ b/components/auth/signup-wizard.tsx
@@ -0,0 +1,74 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { useRouter, useSearchParams } from "next/navigation"
+import StepIndicator from "./step-indicator"
+import SignupFormStep from "./signup-form-step"
+import OtpVerificationStep from "./otp-verification-step"
+import PasskeySetupStep from "./passkey-setup-step"
+
+export default function SignupWizard() {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const stepParam = searchParams.get("step")
+
+ const [step, setStep] = useState<1 | 2 | 3>(() => {
+ if (stepParam === "otp") return 2
+ if (stepParam === "passkey") return 3
+ return 1
+ })
+ const [email, setEmail] = useState("")
+
+ // Update URL when step changes
+ useEffect(() => {
+ const params = new URLSearchParams()
+ if (step === 2) params.set("step", "otp")
+ if (step === 3) params.set("step", "passkey")
+ const query = params.toString()
+ router.replace(`/signup${query ? `?${query}` : ""}`, {
+ scroll: false,
+ })
+ }, [step, router])
+
+ const handleSignupSuccess = (userEmail: string) => {
+ setEmail(userEmail)
+ setStep(2)
+ }
+
+ const handleOtpSuccess = () => {
+ setStep(3)
+ }
+
+ const handlePasskeyComplete = () => {
+ router.push("/")
+ }
+
+ const handlePasskeySkip = () => {
+ router.push("/")
+ }
+
+ return (
+
+
+
+ {step === 1 && (
+
+ )}
+
+ {step === 2 && (
+
setStep(1)}
+ />
+ )}
+
+ {step === 3 && (
+
+ )}
+
+ )
+}