{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "multi-step-form", "title": "Multi Step Form", "description": "A dynamic, animated multi-step form with validation.", "dependencies": [ "motion", "date-fns", "react-hook-form", "@hookform/resolvers", "zod", "sonner", "lucide-react", "react-use-measure", "clsx", "tailwind-merge" ], "registryDependencies": [ "card", "button", "input", "textarea", "select", "badge", "calendar", "popover" ], "files": [ { "path": "registry/default/example/multi-step-form.tsx", "content": "\"use client\";\r\n\r\nimport React, { useState, useMemo } from \"react\";\r\nimport { format } from \"date-fns\";\r\nimport { Check, ChevronRight, ChevronLeft, CalendarIcon } from \"lucide-react\";\r\nimport { useForm, FormProvider as Form } from \"react-hook-form\";\r\nimport { zodResolver } from \"@hookform/resolvers/zod\";\r\nimport { z } from \"zod\";\r\nimport { toast } from \"sonner\";\r\nimport {\r\n Field,\r\n FieldLabel,\r\n FieldDescription,\r\n FieldError,\r\n} from \"@/components/ui/field\";\r\nimport {\r\n Card,\r\n CardHeader,\r\n CardTitle,\r\n CardDescription,\r\n CardContent,\r\n CardFooter,\r\n} from \"@/components/ui/card\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Calendar } from \"@/components/ui/calendar\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { AnimatePresence, motion, MotionConfig } from \"motion/react\";\r\nimport useMeasure from \"react-use-measure\";\r\n\r\nconst TEAM_SIZE_OPTIONS = [\r\n { label: \"Select team size\", value: null },\r\n { label: \"1-5 Members\", value: \"1-5\" },\r\n { label: \"5-10 Members\", value: \"5-10\" },\r\n { label: \"10+ Members\", value: \"10+\" },\r\n];\r\n\r\nconst PRIORITY_OPTIONS = [\r\n { label: \"Select priority\", value: null },\r\n { label: \"Low\", value: \"Low\" },\r\n { label: \"Medium\", value: \"Medium\" },\r\n { label: \"High\", value: \"High\" },\r\n { label: \"Critical\", value: \"Critical\" },\r\n];\r\n\r\nconst formSchema = z.object({\r\n \"project-name\": z.string().optional(),\r\n \"due-date\": z.date().optional(),\r\n description: z.string().optional(),\r\n \"team-size\": z.string().nullable().optional(),\r\n priority: z.string().nullable().optional(),\r\n tag: z.array(z.string()).optional(),\r\n mood: z.string().optional(),\r\n comment: z.string().optional(),\r\n});\r\n\r\ntype FormValues = z.infer;\r\n\r\nexport default function MultiStepFormDemo() {\r\n const [currentStep, setCurrentStep] = useState(0);\r\n const [direction, setDirection] = useState();\r\n const [ref, bounds] = useMeasure();\r\n\r\n const form = useForm({\r\n resolver: zodResolver(formSchema),\r\n defaultValues: {\r\n \"project-name\": \"\",\r\n \"due-date\": undefined,\r\n description: \"\",\r\n \"team-size\": null,\r\n priority: null,\r\n tag: [],\r\n mood: \"\",\r\n comment: \"\",\r\n },\r\n });\r\n\r\n function onSubmit(values: FormValues) {\r\n try {\r\n console.log(values);\r\n toast(\r\n
\r\n          {JSON.stringify(values, null, 2)}\r\n        
\r\n );\r\n } catch (error) {\r\n console.error(\"Form submission error\", error);\r\n toast.error(\"Failed to submit the form. Please try again.\");\r\n }\r\n }\r\n\r\n const nextStep = () => {\r\n if (currentStep === 2) {\r\n form.handleSubmit(onSubmit)();\r\n return;\r\n }\r\n if (currentStep < 2) {\r\n setDirection(1);\r\n setCurrentStep((prev) => prev + 1);\r\n }\r\n };\r\n\r\n const prevStep = () => {\r\n if (currentStep > 0) {\r\n setDirection(-1);\r\n setCurrentStep((prev) => prev - 1);\r\n }\r\n };\r\n\r\n // Change Here\r\n const stepTitles = [\r\n {\r\n title: \"Create New Project\",\r\n description:\r\n \"Start by providing the essential details for your workspace.\",\r\n },\r\n {\r\n title: \"Configuration\",\r\n description: \"Define team access and project priority settings.\",\r\n },\r\n {\r\n title: \"Project Kickoff Mood\",\r\n description: \"How confident do you feel about this new project?\",\r\n },\r\n ];\r\n\r\n const watchedValues = form.watch();\r\n\r\n const content = useMemo(() => {\r\n switch (currentStep) {\r\n case 0:\r\n return (\r\n
\r\n \r\n Project Name\r\n \r\n \r\n {form.formState.errors[\"project-name\"]?.message}\r\n \r\n \r\n\r\n \r\n Due Date\r\n \r\n \r\n \r\n {watchedValues[\"due-date\"] ? (\r\n format(watchedValues[\"due-date\"] as Date, \"PPP\")\r\n ) : (\r\n Pick a date\r\n )}\r\n \r\n }\r\n />\r\n \r\n form.setValue(\"due-date\", date)}\r\n initialFocus\r\n />\r\n \r\n \r\n \r\n {form.formState.errors[\"due-date\"]?.message}\r\n \r\n \r\n\r\n \r\n Description\r\n \r\n \r\n {form.formState.errors.description?.message}\r\n \r\n \r\n
\r\n );\r\n case 1:\r\n return (\r\n
\r\n
\r\n \r\n Team Size\r\n form.setValue(\"team-size\", val)}\r\n >\r\n \r\n \r\n \r\n \r\n {TEAM_SIZE_OPTIONS.map((opt) => (\r\n \r\n {opt.label}\r\n \r\n ))}\r\n \r\n \r\n \r\n {form.formState.errors[\"team-size\"]?.message}\r\n \r\n \r\n\r\n \r\n Priority\r\n form.setValue(\"priority\", val)}\r\n >\r\n \r\n \r\n \r\n \r\n {PRIORITY_OPTIONS.map((opt) => (\r\n \r\n {opt.label}\r\n \r\n ))}\r\n \r\n \r\n \r\n {form.formState.errors.priority?.message}\r\n \r\n \r\n
\r\n\r\n \r\n Tags\r\n
\r\n
\r\n {watchedValues[\"tag\"]?.map((t, i) => (\r\n \r\n {t}\r\n {\r\n const tags = form.getValues(\"tag\") || [];\r\n form.setValue(\r\n \"tag\",\r\n tags.filter((_, index) => index !== i)\r\n );\r\n }}\r\n className=\"hover:text-destructive\"\r\n >\r\n ×\r\n \r\n \r\n ))}\r\n
\r\n {\r\n if (e.key === \"Enter\") {\r\n e.preventDefault();\r\n const val = e.currentTarget.value.trim();\r\n if (val) {\r\n const tags = form.getValues(\"tag\") || [];\r\n if (!tags.includes(val)) {\r\n form.setValue(\"tag\", [...tags, val]);\r\n }\r\n e.currentTarget.value = \"\";\r\n }\r\n }\r\n }}\r\n />\r\n
\r\n {form.formState.errors.tag?.message}\r\n
\r\n
\r\n );\r\n case 2:\r\n return (\r\n
\r\n
\r\n
\r\n {[\r\n { emoji: \"😰\", value: \"anxious\", label: \"Anxious\" },\r\n { emoji: \"😟\", value: \"worried\", label: \"Worried\" },\r\n { emoji: \"😐\", value: \"neutral\", label: \"Neutral\" },\r\n { emoji: \"🙂\", value: \"good\", label: \"Good\" },\r\n { emoji: \"🤩\", value: \"excited\", label: \"Excited\" },\r\n ].map((option) => (\r\n form.setValue(\"mood\", option.value)}\r\n >\r\n {option.emoji}\r\n \r\n ))}\r\n
\r\n \r\n
\r\n

\r\n Your feedback helps us understand the project kickoff vibe.\r\n

\r\n
\r\n );\r\n default:\r\n return null;\r\n }\r\n }, [currentStep, form, watchedValues]);\r\n\r\n const variants = {\r\n initial: (direction: number) => {\r\n return { x: `${110 * direction}%`, opacity: 0 };\r\n },\r\n animate: { x: \"0%\", opacity: 1 },\r\n exit: (direction: number) => {\r\n return { x: `${-110 * direction}%`, opacity: 0 };\r\n },\r\n };\r\n\r\n return (\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n
\r\n \r\n {stepTitles[currentStep].title}\r\n \r\n \r\n {stepTitles[currentStep].description}\r\n \r\n
\r\n
\r\n {stepTitles.map((_, index) => (\r\n \r\n ))}\r\n
\r\n
\r\n\r\n 0 ? bounds.height : \"auto\" }}\r\n className=\"relative overflow-hidden\"\r\n transition={{ type: \"spring\", bounce: 0, duration: 0.5 }}\r\n >\r\n
\r\n \r\n \r\n \r\n {content}\r\n \r\n \r\n \r\n
\r\n
\r\n\r\n \r\n \r\n \r\n Back\r\n \r\n \r\n \r\n \r\n
\r\n
\r\n \r\n \r\n );\r\n}\r\n", "type": "registry:component" } ], "type": "registry:component" }