{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signup-form",
  "type": "registry:block",
  "title": "Sign up form",
  "description": "An account creation flow with field validation, password strength, and a clear completion state.",
  "categories": [
    "blocks"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/button.json",
    "https://irisui.dev/r/checkbox.json",
    "https://irisui.dev/r/input.json",
    "https://irisui.dev/r/password-field.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/blocks/signup-form/signup-form.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useId, useLayoutEffect, useRef, useState, type FormEvent } from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  type Transition,\n  type Variants,\n} from \"motion/react\";\nimport { Button } from \"../../button/button\";\nimport { Checkbox } from \"../../checkbox/checkbox\";\nimport { Input } from \"../../input/input\";\nimport { PasswordField } from \"../../password-field/password-field\";\nimport { motionTokens } from \"../../lib/motion-tokens\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface SignupDetails {\n  name: string;\n  email: string;\n  password: string;\n  productUpdates: boolean;\n}\n\ntype Provider = \"Google\" | \"Apple\" | \"GitHub\";\n\nexport interface SignupFormProps {\n  /** Receives the details once every field is valid. Throw to show a submit error. */\n  onSubmit?: (details: SignupDetails) => void | Promise<void>;\n  /** An error from your API, shown above the submit button. */\n  serverError?: string | null;\n  /** Called when someone picks a single sign-on provider. The preview simulates the redirect. */\n  onProvider?: (provider: Provider) => void;\n}\n\ntype Field = \"name\" | \"email\" | \"password\";\ntype Step = \"form\" | \"done\";\ntype StepCustom = { direction: number; reduce: boolean };\n\n/* Shared class strings. Container steps follow the `signup` container at 380 px. */\nconst stepClass = \"grid min-w-0 p-8 @max-[380px]/signup:px-5 @max-[380px]/signup:py-6\";\nconst headingClass =\n  \"mb-6 grid gap-2 [&_h2]:m-0 [&_h2]:font-display [&_h2]:text-3xl [&_h2]:leading-display [&_h2]:font-normal [&_h2]:tracking-display [&_h2]:text-balance [&_h2]:[outline:none] @max-[380px]/signup:[&_h2]:text-2xl [&_p]:m-0 [&_p]:text-sm [&_p]:leading-body [&_p]:text-pretty [&_p]:text-ink-2\";\nconst textButtonClass =\n  \"cursor-pointer p-0 text-ink-2 underline decoration-line-strong underline-offset-3 transition-[color,text-decoration-color] duration-fast ease-standard hover:text-ink hover:decoration-current motion-reduce:transition-none\";\n/** Lit meter bars take the tone of the score: weak red, fair orange, good accent, strong green. */\nconst meterTone: Partial<Record<number, string>> = { 1: \"bg-red\", 2: \"bg-orange\", 4: \"bg-green\" };\n\nconst providers: Provider[] = [\"Google\", \"Apple\", \"GitHub\"];\n/** The official four-colour Google \"G\"; Apple and GitHub ship monochrome marks, drawn in currentColor. */\nfunction GoogleLogo() {\n  return (\n    <svg className=\"flex-none\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n      <path\n        fill=\"#4285F4\"\n        d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"\n      />\n      <path\n        fill=\"#34A853\"\n        d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"\n      />\n      <path\n        fill=\"#FBBC05\"\n        d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"\n      />\n      <path\n        fill=\"#EA4335\"\n        d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"\n      />\n    </svg>\n  );\n}\nconst monoMarks: Record<Exclude<Provider, \"Google\">, string> = {\n  Apple:\n    \"M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701\",\n  GitHub:\n    \"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\",\n};\n\nfunction getErrors(name: string, email: string, password: string) {\n  return {\n    name: name.trim().length < 2 ? \"Enter your full name.\" : \"\",\n    email: !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/.test(email.trim()) ? \"Enter a full address, like name@example.com.\" : \"\",\n    password: password.length < 8 ? \"Use at least 8 characters.\" : \"\",\n  };\n}\n\nfunction passwordScore(password: string) {\n  if (!password) return 0;\n  return [\n    password.length >= 8,\n    password.length >= 12,\n    /[a-z]/.test(password) && /[A-Z]/.test(password),\n    /\\d|[^\\w\\s]/.test(password),\n  ].filter(Boolean).length;\n}\nconst strengthLabel = [\"\", \"Weak\", \"Fair\", \"Good\", \"Strong\"];\n\n/** Steps cross over sideways out of a small blur; going back, they return from the other side. */\nconst stepMotion: Variants = {\n  enter: ({ direction, reduce }: StepCustom) =>\n    reduce ? { opacity: 0 } : { opacity: 0, x: direction * 24, filter: `blur(${motionTokens.blur.soft}px)` },\n  center: ({ reduce }: StepCustom) => ({\n    opacity: 1,\n    x: 0,\n    filter: \"blur(0px)\",\n    transition: reduce\n      ? { duration: motionTokens.duration.instant }\n      : {\n          x: motionTokens.spring.smooth,\n          opacity: { duration: motionTokens.duration.standard, ease: motionTokens.ease.enter, delay: 0.05 },\n          filter: { duration: motionTokens.duration.standard, ease: motionTokens.ease.enter },\n        },\n  }),\n  exit: ({ direction, reduce }: StepCustom) =>\n    reduce\n      ? { opacity: 0, transition: { duration: 0 } }\n      : {\n          opacity: 0,\n          x: direction * -16,\n          filter: `blur(${motionTokens.blur.soft}px)`,\n          transition: {\n            x: motionTokens.spring.smooth,\n            opacity: { duration: motionTokens.duration.exit, ease: motionTokens.ease.standard },\n            filter: { duration: motionTokens.duration.exit },\n          },\n        },\n};\nconst swap = (reduce: boolean) => ({\n  initial: reduce ? { opacity: 0 } : { opacity: 0, y: 6, filter: `blur(${motionTokens.blur.soft}px)` },\n  animate: { opacity: 1, y: 0, filter: \"blur(0px)\" },\n  exit: reduce\n    ? { opacity: 0, transition: { duration: 0 } }\n    : {\n        opacity: 0,\n        y: -4,\n        filter: `blur(${motionTokens.blur.subtle}px)`,\n        transition: { duration: motionTokens.duration.fast },\n      },\n  transition: (reduce\n    ? { duration: motionTokens.duration.instant }\n    : { duration: motionTokens.duration.standard, ease: motionTokens.ease.enter }) as Transition,\n});\n\n/** The card follows its content on a spring only while the step changes; otherwise it stays auto, so field messages open without lag. */\nfunction useStepHeight(step: Step, reduce: boolean) {\n  const track = useRef<HTMLDivElement>(null);\n  const height = useMotionValue<number | \"auto\">(\"auto\");\n  const measured = useRef(0);\n  const gliding = useRef(false);\n  const lastStep = useRef(step);\n  const glide = useCallback(\n    (to: number) => {\n      gliding.current = true;\n      animate(height, to, {\n        ...motionTokens.spring.smooth,\n        onComplete: () => {\n          gliding.current = false;\n          height.jump(\"auto\");\n        },\n      });\n    },\n    [height],\n  );\n  useEffect(() => {\n    const node = track.current;\n    if (!node || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(([entry]) => {\n      if (!entry) return;\n      measured.current = entry.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight;\n      if (gliding.current) glide(measured.current);\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [glide]);\n  useLayoutEffect(() => {\n    if (lastStep.current === step) return;\n    lastStep.current = step;\n    const current = height.get();\n    const from = typeof current === \"number\" ? current : measured.current;\n    const to = track.current?.offsetHeight ?? 0;\n    if (reduce || !from || !to) {\n      gliding.current = false;\n      height.jump(\"auto\");\n      return;\n    }\n    if (current === \"auto\") height.jump(from);\n    glide(to);\n  }, [step, reduce, height, glide]);\n  return { track, height };\n}\n\nexport function SignupForm({ onSubmit, serverError, onProvider }: SignupFormProps) {\n  const reduce = !!useReducedMotion();\n  const feedbackId = useId();\n  const nameRef = useRef<HTMLInputElement>(null);\n  const emailRef = useRef<HTMLInputElement>(null);\n  const passwordRef = useRef<HTMLInputElement>(null);\n  const doneRef = useRef<HTMLHeadingElement>(null);\n  const timers = useRef<number[]>([]);\n  const [step, setStep] = useState<Step>(\"form\");\n  const [direction, setDirection] = useState(1);\n  const [name, setName] = useState(\"\");\n  const [email, setEmail] = useState(\"\");\n  const [password, setPassword] = useState(\"\");\n  const [productUpdates, setProductUpdates] = useState(false);\n  const [touched, setTouched] = useState<Record<Field, boolean>>({ name: false, email: false, password: false });\n  const [submitted, setSubmitted] = useState(false);\n  const [busy, setBusy] = useState<null | \"form\" | Provider>(null);\n  const [method, setMethod] = useState<\"Email\" | Provider>(\"Email\");\n  const [submitError, setSubmitError] = useState(\"\");\n  const [status, setStatus] = useState(\"\");\n  const { track, height } = useStepHeight(step, reduce);\n  const errors = getErrors(name, email, password);\n  const score = passwordScore(password);\n  const visibleError = (field: Field) => (touched[field] || submitted ? errors[field] : \"\");\n  const passwordError = visibleError(\"password\");\n  const custom: StepCustom = { direction, reduce };\n  const shownError = submitError || serverError || \"\";\n  const feedback = passwordError\n    ? { key: \"error\", text: passwordError }\n    : password\n      ? { key: `s${score}`, text: `${strengthLabel[Math.max(score, 1)]} password` }\n      : { key: \"hint\", text: \"Use 8 or more characters.\" };\n\n  useEffect(() => {\n    const pending = timers.current;\n    return () => pending.forEach(window.clearTimeout);\n  }, []);\n  useEffect(() => {\n    if (step === \"done\") doneRef.current?.focus();\n  }, [step]);\n\n  function markTouched(field: Field) {\n    setTouched((current) => ({ ...current, [field]: true }));\n  }\n\n  function finish(how: \"Email\" | Provider) {\n    setBusy(null);\n    setMethod(how);\n    setDirection(1);\n    setStep(\"done\");\n    setStatus(`Account created with ${how === \"Email\" ? \"your email\" : how}`);\n  }\n\n  async function submit(event: FormEvent<HTMLFormElement>) {\n    event.preventDefault();\n    if (busy) return;\n    setSubmitted(true);\n    setSubmitError(\"\");\n    const nextErrors = getErrors(name, email, password);\n    const firstInvalid = (Object.keys(nextErrors) as Field[]).find((field) => nextErrors[field]);\n    if (firstInvalid) {\n      ({ name: nameRef, email: emailRef, password: passwordRef })[firstInvalid].current?.focus();\n      return;\n    }\n    setBusy(\"form\");\n    setStatus(\"Creating your account\");\n    try {\n      await Promise.all([\n        onSubmit?.({ name: name.trim(), email: email.trim(), password, productUpdates }),\n        new Promise((resolve) => window.setTimeout(resolve, 700)),\n      ]);\n      finish(\"Email\");\n    } catch (error) {\n      setBusy(null);\n      setSubmitError(\n        error instanceof Error && error.message ? error.message : \"We couldn't create your account. Try again.\",\n      );\n      setStatus(\"Account not created\");\n    }\n  }\n\n  function continueWith(provider: Provider) {\n    if (busy) return;\n    setBusy(provider);\n    setStatus(`Opening ${provider}`);\n    onProvider?.(provider);\n    timers.current.push(\n      window.setTimeout(() => {\n        if (!name.trim()) setName(\"Dafni Rokka\");\n        if (!email.trim()) setEmail(\"dafni@mail.example\");\n        finish(provider);\n      }, 900),\n    );\n  }\n\n  function reset() {\n    setName(\"\");\n    setEmail(\"\");\n    setPassword(\"\");\n    setProductUpdates(false);\n    setTouched({ name: false, email: false, password: false });\n    setSubmitted(false);\n    setSubmitError(\"\");\n    setDirection(-1);\n    setStep(\"form\");\n    setStatus(\"Start again with new details\");\n    timers.current.push(window.setTimeout(() => nameRef.current?.focus(), 60));\n  }\n\n  const firstName = name.trim().split(/\\s+/)[0] || \"there\";\n\n  return (\n    <section\n      className=\"w-[min(100%,432px)] min-w-0 overflow-hidden rounded-surface border border-line bg-surface font-body tracking-body text-ink @container/signup [@media(max-width:480px)]:rounded-panel\"\n      aria-label=\"Create a MyRoomie account\"\n    >\n      <motion.div className=\"overflow-hidden\" style={{ height }}>\n        <div ref={track} className=\"relative\">\n          <AnimatePresence mode=\"popLayout\" initial={false} custom={custom}>\n            {step === \"form\" ? (\n              <motion.div\n                key=\"form\"\n                className={stepClass}\n                custom={custom}\n                variants={stepMotion}\n                initial=\"enter\"\n                animate=\"center\"\n                exit=\"exit\"\n              >\n                <div className={headingClass}>\n                  <h2>Create your account</h2>\n                  <p>Find a room or a flatmate. You can set up your profile after.</p>\n                </div>\n                <fieldset className=\"grid min-w-0 grid-cols-3 gap-2\" aria-label=\"Sign up with\">\n                  {providers.map((provider) => (\n                    <Button\n                      key={provider}\n                      type=\"button\"\n                      variant=\"secondary\"\n                      className=\"min-w-0 px-2 @max-[380px]/signup:gap-1.5 @max-[380px]/signup:text-xs\"\n                      aria-label={`Sign up with ${provider}`}\n                      loading={busy === provider}\n                      aria-disabled={(!!busy && busy !== provider) || undefined}\n                      onClick={() => continueWith(provider)}\n                    >\n                      {provider === \"Google\" ? (\n                        <GoogleLogo />\n                      ) : (\n                        <svg\n                          className=\"flex-none\"\n                          width=\"16\"\n                          height=\"16\"\n                          viewBox=\"0 0 24 24\"\n                          fill=\"currentColor\"\n                          aria-hidden=\"true\"\n                        >\n                          <path d={monoMarks[provider]} />\n                        </svg>\n                      )}\n                      {provider}\n                    </Button>\n                  ))}\n                </fieldset>\n                <div className=\"mx-0 my-5 flex items-center gap-3 text-xs/none text-ink-3 before:h-px before:flex-1 before:bg-line before:content-[''] after:h-px after:flex-1 after:bg-line after:content-['']\">\n                  or\n                </div>\n                <form onSubmit={(event) => void submit(event)} noValidate className=\"grid gap-4\">\n                  <Input\n                    ref={nameRef}\n                    label=\"Full name\"\n                    name=\"name\"\n                    autoComplete=\"name\"\n                    placeholder=\"Dafni Rokka\"\n                    value={name}\n                    onChange={(event) => setName(event.target.value)}\n                    onBlur={() => {\n                      if (name) markTouched(\"name\");\n                    }}\n                    error={visibleError(\"name\")}\n                    readOnly={!!busy}\n                  />\n                  <Input\n                    ref={emailRef}\n                    label=\"Email\"\n                    name=\"email\"\n                    type=\"email\"\n                    inputMode=\"email\"\n                    autoComplete=\"email\"\n                    autoCapitalize=\"none\"\n                    spellCheck={false}\n                    placeholder=\"name@example.com\"\n                    value={email}\n                    onChange={(event) => setEmail(event.target.value)}\n                    onBlur={() => {\n                      if (email) markTouched(\"email\");\n                    }}\n                    error={visibleError(\"email\")}\n                    readOnly={!!busy}\n                  />\n                  <div className=\"grid min-w-0\">\n                    <PasswordField\n                      ref={passwordRef}\n                      label=\"Password\"\n                      name=\"password\"\n                      autoComplete=\"new-password\"\n                      value={password}\n                      onChange={(event) => setPassword(event.target.value)}\n                      onBlur={() => {\n                        if (password) markTouched(\"password\");\n                      }}\n                      aria-invalid={passwordError ? true : undefined}\n                      aria-describedby={feedbackId}\n                      readOnly={!!busy}\n                    />\n                    <div className=\"mt-2 flex min-h-[18px] items-center justify-between gap-3 text-xs leading-body\">\n                      <span\n                        id={feedbackId}\n                        className={cn(\"relative min-w-0 flex-1 *:block\", passwordError ? \"text-red\" : \"text-ink-3\")}\n                        data-error={passwordError ? true : undefined}\n                        role={passwordError ? \"alert\" : undefined}\n                      >\n                        <AnimatePresence mode=\"popLayout\" initial={false}>\n                          <motion.span key={feedback.key} {...swap(reduce)}>\n                            {feedback.text}\n                          </motion.span>\n                        </AnimatePresence>\n                      </span>\n                      <span className=\"flex flex-none gap-[3px]\" data-level={score} aria-hidden=\"true\">\n                        {[1, 2, 3, 4].map((bar) => (\n                          <span\n                            key={bar}\n                            className={cn(\n                              \"h-[3px] w-5 rounded-full transition-[background-color] duration-fast ease-standard motion-reduce:transition-none @max-[380px]/signup:w-3.5\",\n                              bar <= score ? (meterTone[score] ?? \"bg-accent\") : \"bg-line\",\n                            )}\n                            data-on={bar <= score || undefined}\n                          />\n                        ))}\n                      </span>\n                    </div>\n                  </div>\n                  <div className=\"-my-2 mr-0 ml-[calc((var(--control-height-md)_-_18px)/-2)]\">\n                    <Checkbox\n                      label=\"Email me product updates\"\n                      checked={productUpdates}\n                      onCheckedChange={(checked) => setProductUpdates(checked === true)}\n                      disabled={!!busy}\n                    />\n                  </div>\n                  <AnimatePresence initial={false}>\n                    {shownError && (\n                      <motion.div\n                        key=\"error\"\n                        className=\"overflow-hidden\"\n                        initial={{ height: 0, opacity: 0 }}\n                        animate={{ height: \"auto\", opacity: 1 }}\n                        exit={{ height: 0, opacity: 0 }}\n                        transition={\n                          reduce\n                            ? { duration: 0 }\n                            : { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.fast } }\n                        }\n                      >\n                        <p className=\"m-0 text-xs leading-body text-red\" role=\"alert\">\n                          {shownError}\n                        </p>\n                      </motion.div>\n                    )}\n                  </AnimatePresence>\n                  <Button className=\"w-full\" type=\"submit\" loading={busy === \"form\"}>\n                    Create account\n                  </Button>\n                </form>\n                <p className=\"mx-0 mt-4 mb-0 text-xs leading-body text-pretty text-ink-3\">\n                  By creating an account you agree to the{\" \"}\n                  <button type=\"button\" className={textButtonClass} onClick={() => setStatus(\"Terms open in your app\")}>\n                    Terms\n                  </button>{\" \"}\n                  and{\" \"}\n                  <button\n                    type=\"button\"\n                    className={textButtonClass}\n                    onClick={() => setStatus(\"Privacy policy opens in your app\")}\n                  >\n                    Privacy policy\n                  </button>\n                  .\n                </p>\n              </motion.div>\n            ) : (\n              <motion.div\n                key=\"done\"\n                className={stepClass}\n                custom={custom}\n                variants={stepMotion}\n                initial=\"enter\"\n                animate=\"center\"\n                exit=\"exit\"\n              >\n                <svg className=\"mb-6 block size-12 overflow-visible text-green\" viewBox=\"0 0 48 48\" aria-hidden=\"true\">\n                  <g transform=\"rotate(-90 24 24)\">\n                    <motion.circle\n                      cx=\"24\"\n                      cy=\"24\"\n                      r=\"22.25\"\n                      fill=\"none\"\n                      stroke=\"currentColor\"\n                      strokeWidth=\"1.5\"\n                      initial={reduce ? false : { pathLength: 0, opacity: 0 }}\n                      animate={{ pathLength: 1, opacity: 1 }}\n                      transition={{\n                        pathLength: {\n                          duration: motionTokens.duration.considered,\n                          ease: motionTokens.ease.inOut,\n                          delay: 0.16,\n                        },\n                        opacity: { duration: motionTokens.duration.instant, delay: 0.16 },\n                      }}\n                    />\n                  </g>\n                  <motion.path\n                    d=\"M15.5 24.5l5.5 5.5 11.5-12\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"2\"\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    initial={reduce ? false : { pathLength: 0, opacity: 0 }}\n                    animate={{ pathLength: 1, opacity: 1 }}\n                    transition={{\n                      pathLength: {\n                        duration: motionTokens.duration.standard + 0.08,\n                        ease: motionTokens.ease.enter,\n                        delay: 0.5,\n                      },\n                      opacity: { duration: motionTokens.duration.instant, delay: 0.5 },\n                    }}\n                  />\n                </svg>\n                <div className={headingClass}>\n                  <h2 ref={doneRef} tabIndex={-1}>\n                    Welcome, {firstName}\n                  </h2>\n                  <p>\n                    Your account is ready. We sent a confirmation to{\" \"}\n                    <span className=\"text-ink [overflow-wrap:anywhere]\">{email.trim()}</span>.\n                  </p>\n                </div>\n                <dl className=\"mx-0 mt-0 mb-6 grid border-t border-line p-0 [&_dd]:m-0 [&_dd]:text-right [&_div]:flex [&_div]:justify-between [&_div]:gap-4 [&_div]:border-b [&_div]:border-line [&_div]:py-3 [&_div]:text-sm [&_div]:leading-body [&_dt]:text-ink-2\">\n                  <div>\n                    <dt>Signed up with</dt>\n                    <dd>{method}</dd>\n                  </div>\n                  <div>\n                    <dt>Product updates</dt>\n                    <dd>{productUpdates ? \"On\" : \"Off\"}</dd>\n                  </div>\n                </dl>\n                <Button type=\"button\" variant=\"secondary\" className=\"w-full\" onClick={reset}>\n                  Start over\n                </Button>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n      </motion.div>\n\n      <footer className=\"flex min-h-12 flex-wrap items-center justify-between gap-x-4 gap-y-1 border-t border-line px-8 py-3 text-xs leading-body text-ink-3 @max-[380px]/signup:px-5\">\n        <output className=\"sr-only\" aria-live=\"polite\">\n          {status}\n        </output>\n        <span\n          className=\"relative block min-w-0 flex-[1_1_12rem] *:block *:overflow-hidden *:text-ellipsis *:whitespace-nowrap\"\n          aria-hidden=\"true\"\n        >\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            <motion.span key={status} {...swap(reduce)}>\n              {status}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n        <span className=\"whitespace-nowrap text-ink-2\">\n          Have an account?{\" \"}\n          <button\n            type=\"button\"\n            className={cn(textButtonClass, \"font-medium text-ink decoration-transparent\")}\n            onClick={() => setStatus(\"Sign in opens in your app\")}\n          >\n            Sign in\n          </button>\n        </span>\n      </footer>\n    </section>\n  );\n}\n\nexport default SignupForm;\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/signup-form/signup-form.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/signup-form",
  "meta": {
    "kind": "block",
    "group": "Blocks",
    "export": "SignupForm",
    "docs": "https://irisui.dev/components/signup-form"
  }
}
