"use client"; import { useEffect, useRef, useState, type CSSProperties, type FormEvent, type ReactNode } from "react"; import { motion, useReducedMotion } from "motion/react"; export type MultiStepFormStep = { title: string; /** Small text under the title. */ description?: string; /** The fields of this step. Give each a `name` and use normal rules like `required`. */ content: ReactNode; }; type MultiStepFormProps = { steps: MultiStepFormStep[]; /** Called with every field from every step after the last step passes. */ onSubmit?: (data: FormData) => void; /** Label of the last button. */ submitLabel?: string; /** Shown once the form is sent. */ success?: ReactNode; /** Rail and button colour. */ accent?: string; /** Wrapper look (width etc.). */ className?: string; }; const FIELDS = "input:not([type=hidden]),select,textarea"; /* MultiStepForm: one form split into steps. A rail fills as you go, steps slide in the direction you move, and each step must pass its normal field rules before the next opens. Every step stays mounted, so typed values are kept when going back. */ export function MultiStepForm({ steps, onSubmit, submitLabel = "Submit", success = "All done. Thank you!", accent = "#a78bfa", className = "w-full max-w-md", }: MultiStepFormProps) { const reduce = useReducedMotion(); const panels = useRef<(HTMLDivElement | null)[]>([]); const moved = useRef(false); const doneBox = useRef(null); const [step, setStep] = useState(0); const [done, setDone] = useState(false); const last = step === steps.length - 1; const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 340, damping: 34 }; useEffect(() => { if (moved.current) panels.current[step]?.querySelector(FIELDS)?.focus({ preventScroll: true }); }, [step]); useEffect(() => { if (done) doneBox.current?.focus(); }, [done]); const go = (to: number) => { moved.current = true; setStep(to); }; const submit = (e: FormEvent) => { e.preventDefault(); const fields = panels.current[step]?.querySelectorAll(FIELDS) ?? []; if (!Array.from(fields).every((f) => f.reportValidity())) return; if (!last) return go(step + 1); onSubmit?.(new FormData(e.currentTarget)); setDone(true); }; return (
{done ? (
{success}
) : (
    {steps.map((s, i) => (
  1. ))}
{steps.map((s, i) => { const here = i === step; return ( { panels.current[i] = el; }} inert={!here} initial={false} animate={{ x: reduce ? 0 : (i - step) * 48, opacity: here ? 1 : 0 }} transition={spring} className={`min-w-0 p-0.5 [grid-area:1/1] ${here ? "visible" : "invisible delay-300 motion-reduce:delay-0"} transition-[visibility] duration-0`} >

{s.title}

{s.description &&

{s.description}

}
{s.content}
); })}

Step {step + 1} of {steps.length}

)}
); }