{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-step-form",
  "type": "registry:ui",
  "title": "Multi-Step Form",
  "description": "A typed, multi-stage form with per-step validation, direction-aware transitions and a success state.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/multi-step-form.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\n/* -------------------------------------------------------------------------- */\n/*  Public types                                                              */\n/* -------------------------------------------------------------------------- */\n\nexport type MultiStepFormValues = Record<string, string>;\n\nexport type MultiStepFieldType = \"text\" | \"email\" | \"tel\" | \"url\" | \"number\" | \"textarea\" | \"select\" | \"radio\";\n\nexport interface MultiStepFieldOption {\n  label: string;\n  value: string;\n  /** Secondary line, shown on radio cards. */\n  description?: string;\n}\n\nexport interface MultiStepField {\n  /** Key in the submitted values; also the input `name`. */\n  name: string;\n  label: string;\n  type?: MultiStepFieldType;\n  required?: boolean;\n  placeholder?: string;\n  hint?: string;\n  autoComplete?: string;\n  /** Options for `select` and `radio` fields. */\n  options?: MultiStepFieldOption[];\n  /** Custom validation. Return an error message, or nothing when valid. */\n  validate?: (value: string, values: MultiStepFormValues) => string | null | undefined;\n}\n\nexport interface MultiStepFormStep {\n  id: string;\n  title: string;\n  description?: string;\n  fields: MultiStepField[];\n}\n\nexport interface MultiStepFormProps extends Omit<React.ComponentPropsWithoutRef<\"form\">, \"onSubmit\" | \"children\"> {\n  steps: MultiStepFormStep[];\n  /** Called with all values after the last step validates. May return a promise. */\n  onSubmit?: (values: MultiStepFormValues) => void | Promise<void>;\n  initialValues?: MultiStepFormValues;\n  /** Called whenever the visible step changes. */\n  onStepChange?: (index: number, step: MultiStepFormStep) => void;\n  /** Accent colour (any CSS colour). */\n  accent?: string;\n  /** Step transition style. */\n  transition?: \"slide\" | \"fade\";\n  /** Progress indicator style. */\n  progress?: \"steps\" | \"bar\";\n  /** Step transition duration in ms. */\n  duration?: number;\n  nextLabel?: string;\n  backLabel?: string;\n  submitLabel?: string;\n  successTitle?: string;\n  successMessage?: string;\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Validation                                                                */\n/* -------------------------------------------------------------------------- */\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/;\n\nfunction validateField(field: MultiStepField, values: MultiStepFormValues): string | undefined {\n  const raw = values[field.name] ?? \"\";\n  const value = raw.trim();\n  const choice = field.type === \"radio\" || field.type === \"select\";\n  if (field.required && !value) return choice ? `Choose ${field.label.toLowerCase()}.` : `${field.label} is required.`;\n  if (value && field.type === \"email\" && !EMAIL.test(value)) return \"Enter a valid email address.\";\n  if (value && field.type === \"url\") {\n    try {\n      new URL(value);\n    } catch {\n      return \"Enter a full URL, including https://.\";\n    }\n  }\n  if (value && field.type === \"number\" && !Number.isFinite(Number(value))) return \"Enter a number.\";\n  return field.validate?.(raw, values) ?? undefined;\n}\n\nfunction validateStep(step: MultiStepFormStep, values: MultiStepFormValues) {\n  const errors: Record<string, string> = {};\n  for (const field of step.fields) {\n    const error = validateField(field, values);\n    if (error) errors[field.name] = error;\n  }\n  return errors;\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Fields                                                                    */\n/* -------------------------------------------------------------------------- */\n\nconst controlClass =\n  \"block w-full rounded-lg border bg-white px-3.5 text-[15px] outline-none transition-[border-color,box-shadow] duration-150 placeholder:text-zinc-400 dark:bg-zinc-950 dark:placeholder:text-zinc-500 motion-reduce:transition-none\";\nconst textClass = \"text-zinc-950 dark:text-zinc-50\";\n\ninterface FieldProps {\n  field: MultiStepField;\n  id: string;\n  value: string;\n  error?: string;\n  onChange: (name: string, value: string) => void;\n}\n\nfunction Field({ field, id, value, error, onChange }: FieldProps) {\n  const hintId = `${id}-hint`;\n  const errorId = `${id}-error`;\n  const describedBy = [field.hint ? hintId : null, error ? errorId : null].filter(Boolean).join(\" \") || undefined;\n  const border = error\n    ? \"border-rose-500/80 focus:border-rose-500 focus:shadow-[0_0_0_3px_rgba(244,63,94,0.18)] dark:border-rose-400/80 dark:focus:border-rose-400\"\n    : \"border-zinc-900/15 hover:border-zinc-900/30 focus:border-[var(--msf-accent)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--msf-accent)_22%,transparent)] dark:border-white/15 dark:hover:border-white/30\";\n  const type = field.type ?? \"text\";\n\n  const shared = {\n    id,\n    name: field.name,\n    required: field.required,\n    \"aria-invalid\": error ? true : undefined,\n    \"aria-describedby\": describedBy,\n  };\n\n  const labelEl = (\n    <span className=\"flex items-baseline justify-between gap-3\">\n      <span className=\"text-sm font-medium tracking-tight text-zinc-900 dark:text-zinc-100\">{field.label}</span>\n      {!field.required && (\n        <span className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-400 dark:text-zinc-500\">Optional</span>\n      )}\n    </span>\n  );\n\n  const messages = (\n    <>\n      {field.hint && (\n        <p id={hintId} className=\"mt-1.5 text-[13px] leading-snug text-zinc-500 dark:text-zinc-400\">\n          {field.hint}\n        </p>\n      )}\n      {error && (\n        <p id={errorId} className=\"mt-1.5 text-[13px] font-medium leading-snug text-rose-600 dark:text-rose-400\">\n          {error}\n        </p>\n      )}\n    </>\n  );\n\n  if (type === \"radio\") {\n    const labelId = `${id}-label`;\n    return (\n      <div>\n        <div id={labelId} className=\"mb-2\">\n          {labelEl}\n        </div>\n        <div\n          role=\"radiogroup\"\n          aria-labelledby={labelId}\n          aria-required={field.required || undefined}\n          aria-invalid={error ? true : undefined}\n          aria-describedby={describedBy}\n          className=\"grid gap-2 sm:grid-cols-2\"\n        >\n          {(field.options ?? []).map((option, i) => {\n            const checked = value === option.value;\n            return (\n              <label\n                key={option.value}\n                className={`relative flex cursor-pointer flex-col rounded-lg border px-3.5 py-3 transition-[border-color,background-color,box-shadow] duration-150 has-[:focus-visible]:shadow-[0_0_0_3px_color-mix(in_srgb,var(--msf-accent)_30%,transparent)] motion-reduce:transition-none ${\n                  checked\n                    ? \"border-[var(--msf-accent)] bg-[color-mix(in_srgb,var(--msf-accent)_8%,transparent)]\"\n                    : `${error ? \"border-rose-500/60 dark:border-rose-400/60\" : \"border-zinc-900/15 dark:border-white/15\"} bg-white hover:border-zinc-900/30 dark:bg-zinc-950 dark:hover:border-white/30`\n                }`}\n              >\n                <input\n                  type=\"radio\"\n                  // The first radio carries the field id so focusing \"the field\" lands in the group.\n                  id={i === 0 ? id : undefined}\n                  name={field.name}\n                  value={option.value}\n                  checked={checked}\n                  onChange={() => onChange(field.name, option.value)}\n                  className=\"sr-only\"\n                />\n                <span className=\"flex items-center justify-between gap-2 text-sm font-medium text-zinc-900 dark:text-zinc-100\">\n                  {option.label}\n                  <span\n                    aria-hidden=\"true\"\n                    className={`grid size-4 place-items-center rounded-full border transition-colors ${\n                      checked ? \"border-[var(--msf-accent)] bg-[var(--msf-accent)]\" : \"border-zinc-900/25 dark:border-white/25\"\n                    }`}\n                  >\n                    {checked && <span className=\"size-1.5 rounded-full bg-white\" />}\n                  </span>\n                </span>\n                {option.description && (\n                  <span className=\"mt-0.5 text-[13px] text-zinc-500 dark:text-zinc-400\">{option.description}</span>\n                )}\n              </label>\n            );\n          })}\n        </div>\n        {messages}\n      </div>\n    );\n  }\n\n  return (\n    <div>\n      <label htmlFor={id} className=\"mb-2 block\">\n        {labelEl}\n      </label>\n      {type === \"textarea\" ? (\n        <textarea\n          {...shared}\n          rows={4}\n          placeholder={field.placeholder}\n          value={value}\n          onChange={(e) => onChange(field.name, e.target.value)}\n          className={`${controlClass} ${textClass} ${border} min-h-28 resize-y py-2.5`}\n        />\n      ) : type === \"select\" ? (\n        <div className=\"relative\">\n          <select\n            {...shared}\n            value={value}\n            onChange={(e) => onChange(field.name, e.target.value)}\n            className={`${controlClass} ${border} h-11 appearance-none pr-10 ${value ? textClass : \"text-zinc-400 dark:text-zinc-500\"}`}\n          >\n            <option value=\"\" disabled>\n              {field.placeholder ?? \"Select…\"}\n            </option>\n            {(field.options ?? []).map((o) => (\n              <option key={o.value} value={o.value} className=\"text-zinc-950\">\n                {o.label}\n              </option>\n            ))}\n          </select>\n          <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"pointer-events-none absolute right-3.5 top-1/2 size-4 -translate-y-1/2 text-zinc-400\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\">\n            <path d=\"m4 6 4 4 4-4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n          </svg>\n        </div>\n      ) : (\n        <input\n          {...shared}\n          type={type}\n          inputMode={type === \"number\" ? \"decimal\" : undefined}\n          autoComplete={field.autoComplete}\n          placeholder={field.placeholder}\n          value={value}\n          onChange={(e) => onChange(field.name, e.target.value)}\n          className={`${controlClass} ${textClass} ${border} h-11`}\n        />\n      )}\n      {messages}\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Form                                                                      */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A multi-stage form driven by a typed `steps` config. Each step validates before advancing,\n * transitions are direction-aware, focus moves to the new step's heading (or the first invalid\n * field), and the final step resolves into a success state.\n */\nexport function MultiStepForm({\n  steps,\n  onSubmit,\n  initialValues,\n  onStepChange,\n  accent = \"#FF7448\",\n  transition = \"slide\",\n  progress = \"steps\",\n  duration = 320,\n  nextLabel = \"Continue\",\n  backLabel = \"Back\",\n  submitLabel = \"Send enquiry\",\n  successTitle = \"Thanks — we'll be in touch.\",\n  successMessage = \"Your details are safely with us. Expect a reply within two working days.\",\n  className = \"\",\n  style,\n  ...props\n}: MultiStepFormProps) {\n  const reduceMotion = useReducedMotion();\n  const uid = React.useId();\n  const formRef = React.useRef<HTMLFormElement>(null);\n  const contentRef = React.useRef<HTMLDivElement>(null);\n  const focusPending = React.useRef(false);\n\n  const [index, setIndex] = React.useState(0);\n  const [direction, setDirection] = React.useState(1);\n  const [values, setValues] = React.useState<MultiStepFormValues>(() => ({ ...initialValues }));\n  const [errors, setErrors] = React.useState<Record<string, string>>({});\n  const [status, setStatus] = React.useState<\"editing\" | \"submitting\" | \"success\">(\"editing\");\n  const [submitError, setSubmitError] = React.useState<string | null>(null);\n  const [height, setHeight] = React.useState<number | \"auto\">(\"auto\");\n\n  const total = steps.length;\n  const step = steps[Math.min(index, total - 1)];\n  const isLast = index === total - 1;\n  const done = status === \"success\";\n  const fieldId = (name: string) => `${uid}-${name}`;\n\n  // Measure the active panel so the frame can animate between step heights.\n  React.useEffect(() => {\n    const el = contentRef.current;\n    if (!el) return;\n    const ro = new ResizeObserver(([entry]) => setHeight(entry.borderBoxSize?.[0]?.blockSize ?? el.offsetHeight));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  /** Focuses a step heading once it mounts — only after user navigation, never on first render. */\n  const headingRef = React.useCallback((el: HTMLHeadingElement | null) => {\n    if (el && focusPending.current) {\n      focusPending.current = false;\n      el.focus({ preventScroll: true });\n    }\n  }, []);\n\n  const goTo = (next: number) => {\n    if (next === index || next < 0 || next >= total) return;\n    setDirection(next > index ? 1 : -1);\n    setIndex(next);\n    setSubmitError(null);\n    focusPending.current = true;\n    onStepChange?.(next, steps[next]);\n  };\n\n  const handleChange = (name: string, value: string) => {\n    const nextValues = { ...values, [name]: value };\n    setValues(nextValues);\n    // Once a field shows an error, re-validate it live so the message clears as soon as it's fixed.\n    if (errors[name]) {\n      const field = step.fields.find((f) => f.name === name);\n      const error = field ? validateField(field, nextValues) : undefined;\n      setErrors((prev) => {\n        const copy = { ...prev };\n        if (error) copy[name] = error;\n        else delete copy[name];\n        return copy;\n      });\n    }\n  };\n\n  const focusFirstInvalid = (stepErrors: Record<string, string>) => {\n    const first = step.fields.find((f) => stepErrors[f.name]);\n    if (!first) return;\n    formRef.current?.ownerDocument.getElementById(fieldId(first.name))?.focus();\n  };\n\n  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    if (status !== \"editing\") return;\n\n    const stepErrors = validateStep(step, values);\n    setErrors(stepErrors);\n    if (Object.keys(stepErrors).length) {\n      focusFirstInvalid(stepErrors);\n      return;\n    }\n    if (!isLast) {\n      goTo(index + 1);\n      return;\n    }\n\n    setStatus(\"submitting\");\n    setSubmitError(null);\n    try {\n      await onSubmit?.(values);\n      focusPending.current = true;\n      setDirection(1);\n      setStatus(\"success\");\n    } catch {\n      setStatus(\"editing\");\n      setSubmitError(\"Something went wrong while sending. Please try again.\");\n    }\n  };\n\n  const restart = () => {\n    setValues({ ...initialValues });\n    setErrors({});\n    setDirection(-1);\n    setIndex(0);\n    setStatus(\"editing\");\n    focusPending.current = true;\n    onStepChange?.(0, steps[0]);\n  };\n\n  const seconds = reduceMotion ? 0 : Math.max(0, duration) / 1000;\n  const offset = transition === \"slide\" && !reduceMotion ? 28 : 0;\n  const variants = {\n    enter: (dir: number) => ({ opacity: 0, x: dir * offset }),\n    center: { opacity: 1, x: 0 },\n    exit: (dir: number) => ({ opacity: 0, x: dir * -offset }),\n  };\n  const ease = [0.22, 0.75, 0.2, 1] as const;\n  const progressValue = done ? 1 : (index + 1) / total;\n  const statusText = done ? \"Complete\" : `Step ${index + 1} of ${total}`;\n\n  return (\n    <form\n      ref={formRef}\n      noValidate\n      onSubmit={handleSubmit}\n      aria-busy={status === \"submitting\" || undefined}\n      className={`w-full rounded-2xl border border-zinc-900/10 bg-white/90 p-5 sm:p-7 dark:border-white/10 dark:bg-zinc-950/80 ${className}`}\n      style={{ [\"--msf-accent\" as string]: accent, ...style }}\n      {...props}\n    >\n      {/* Progress */}\n      <div className=\"mb-6\">\n        <div className=\"flex items-center justify-between font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n          <span aria-live=\"polite\" aria-atomic=\"true\">\n            {statusText}\n            <span className=\"sr-only\">{done ? \"\" : `: ${step.title}`}</span>\n          </span>\n          <span aria-hidden=\"true\" className=\"tabular-nums\">\n            {String(Math.round(progressValue * 100)).padStart(2, \"0\")}%\n          </span>\n        </div>\n\n        {progress === \"bar\" ? (\n          <div aria-hidden=\"true\" className=\"mt-3 h-[3px] overflow-hidden rounded-full bg-zinc-900/[0.07] dark:bg-white/10\">\n            <motion.div\n              className=\"h-full origin-left rounded-full bg-[var(--msf-accent)]\"\n              initial={false}\n              animate={{ scaleX: progressValue }}\n              transition={{ duration: seconds * 1.4, ease }}\n            />\n          </div>\n        ) : (\n          <ol className=\"mt-3 flex gap-2\">\n            {steps.map((s, i) => {\n              const state = done || i < index ? \"complete\" : i === index ? \"current\" : \"upcoming\";\n              const content = (\n                <>\n                  <span className=\"relative block h-[3px] w-full overflow-hidden rounded-full bg-zinc-900/[0.07] dark:bg-white/10\">\n                    <motion.span\n                      className=\"absolute inset-0 origin-left rounded-full bg-[var(--msf-accent)]\"\n                      initial={false}\n                      animate={{ scaleX: state === \"upcoming\" ? 0 : 1 }}\n                      transition={{ duration: seconds * 1.2, ease }}\n                    />\n                  </span>\n                  <span\n                    className={`mt-2 block truncate text-left text-[13px] tracking-tight ${\n                      state === \"upcoming\" ? \"text-zinc-400 dark:text-zinc-500\" : \"font-medium text-zinc-900 dark:text-zinc-100\"\n                    }`}\n                  >\n                    <span className=\"mr-1.5 font-mono text-[10px] text-zinc-400 dark:text-zinc-500\">{String(i + 1).padStart(2, \"0\")}</span>\n                    {s.title}\n                  </span>\n                </>\n              );\n              return (\n                <li key={s.id} className=\"min-w-0 flex-1\" aria-current={state === \"current\" ? \"step\" : undefined}>\n                  {state === \"complete\" && !done && status === \"editing\" ? (\n                    // Completed steps can be revisited.\n                    <button\n                      type=\"button\"\n                      onClick={() => goTo(i)}\n                      className=\"block w-full rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--msf-accent)]\"\n                    >\n                      <span className=\"sr-only\">Go back to step {i + 1}: </span>\n                      {content}\n                      <span className=\"sr-only\"> (completed)</span>\n                    </button>\n                  ) : (\n                    <div>\n                      {content}\n                      {state === \"complete\" && <span className=\"sr-only\"> (completed)</span>}\n                    </div>\n                  )}\n                </li>\n              );\n            })}\n          </ol>\n        )}\n      </div>\n\n      {/* Animated frame: height follows the active panel. */}\n      <motion.div\n        className=\"-m-1 overflow-hidden\"\n        initial={false}\n        animate={{ height }}\n        transition={{ duration: seconds, ease }}\n      >\n        <div ref={contentRef} className=\"p-1\">\n          <AnimatePresence mode=\"wait\" initial={false} custom={direction}>\n            {done ? (\n              <motion.div\n                key=\"success\"\n                custom={direction}\n                variants={variants}\n                initial=\"enter\"\n                animate=\"center\"\n                exit=\"exit\"\n                transition={{ duration: seconds, ease }}\n                className=\"flex flex-col items-start py-2\"\n              >\n                <span className=\"grid size-10 place-items-center rounded-full bg-[color-mix(in_srgb,var(--msf-accent)_16%,transparent)] text-[var(--msf-accent)]\">\n                  <svg aria-hidden=\"true\" viewBox=\"0 0 20 20\" className=\"size-5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n                    <motion.path\n                      d=\"m5 10.5 3.2 3L15 6.5\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      initial={{ pathLength: reduceMotion ? 1 : 0 }}\n                      animate={{ pathLength: 1 }}\n                      transition={{ duration: reduceMotion ? 0 : 0.45, delay: seconds, ease }}\n                    />\n                  </svg>\n                </span>\n                <h3 ref={headingRef} tabIndex={-1} className=\"mt-4 text-xl font-medium tracking-tight text-zinc-950 outline-none dark:text-white\">\n                  {successTitle}\n                </h3>\n                <p className=\"mt-1.5 max-w-sm text-sm leading-relaxed text-zinc-600 dark:text-zinc-400\">{successMessage}</p>\n                <button\n                  type=\"button\"\n                  onClick={restart}\n                  className=\"mt-5 text-sm font-medium text-zinc-900 underline decoration-zinc-900/30 underline-offset-4 hover:decoration-zinc-900 focus-visible:rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--msf-accent)] dark:text-zinc-100 dark:decoration-white/30 dark:hover:decoration-white\"\n                >\n                  Start over\n                </button>\n              </motion.div>\n            ) : (\n              <motion.fieldset\n                key={step.id}\n                custom={direction}\n                variants={variants}\n                initial=\"enter\"\n                animate=\"center\"\n                exit=\"exit\"\n                transition={{ duration: seconds, ease }}\n                disabled={status === \"submitting\"}\n                className=\"min-w-0\"\n                aria-labelledby={`${uid}-${step.id}-title`}\n              >\n                <h3\n                  id={`${uid}-${step.id}-title`}\n                  ref={headingRef}\n                  tabIndex={-1}\n                  className=\"text-xl font-medium tracking-tight text-zinc-950 outline-none dark:text-white\"\n                >\n                  {step.title}\n                </h3>\n                {step.description && <p className=\"mt-1 text-sm text-zinc-600 dark:text-zinc-400\">{step.description}</p>}\n                <div className=\"mt-5 flex flex-col gap-5\">\n                  {step.fields.map((field) => (\n                    <Field\n                      key={field.name}\n                      field={field}\n                      id={fieldId(field.name)}\n                      value={values[field.name] ?? \"\"}\n                      error={errors[field.name]}\n                      onChange={handleChange}\n                    />\n                  ))}\n                </div>\n              </motion.fieldset>\n            )}\n          </AnimatePresence>\n        </div>\n      </motion.div>\n\n      {submitError && (\n        <p role=\"alert\" className=\"mt-4 text-[13px] font-medium text-rose-600 dark:text-rose-400\">\n          {submitError}\n        </p>\n      )}\n\n      {!done && (\n        <div className=\"mt-7 flex items-center justify-between gap-3 border-t border-zinc-900/[0.07] pt-5 dark:border-white/10\">\n          <button\n            type=\"button\"\n            onClick={() => goTo(index - 1)}\n            disabled={index === 0 || status === \"submitting\"}\n            className=\"inline-flex h-10 items-center gap-1.5 rounded-lg px-3 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-900/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--msf-accent)] disabled:pointer-events-none disabled:opacity-0 dark:text-zinc-300 dark:hover:bg-white/5\"\n          >\n            <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\">\n              <path d=\"M10 3.5 5.5 8l4.5 4.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n            </svg>\n            {backLabel}\n          </button>\n          <button\n            type=\"submit\"\n            disabled={status === \"submitting\"}\n            className=\"relative inline-flex h-10 items-center gap-1.5 rounded-lg bg-zinc-950 px-4 text-sm font-medium text-white transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--msf-accent)] disabled:cursor-wait disabled:opacity-70 dark:bg-white dark:text-zinc-950\"\n          >\n            {status === \"submitting\" ? (\n              <>\n                <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-4 animate-spin motion-reduce:animate-none\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\">\n                  <path d=\"M14 8a6 6 0 1 1-6-6\" strokeLinecap=\"round\" />\n                </svg>\n                Sending…\n              </>\n            ) : (\n              <>\n                {isLast ? submitLabel : nextLabel}\n                <span aria-hidden=\"true\" className=\"inline-block size-1.5 rounded-full bg-[var(--msf-accent)]\" />\n              </>\n            )}\n          </button>\n        </div>\n      )}\n    </form>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "docs": "Documentation: https://wancraft.my/components/multi-step-form"
}
