{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sign-in",
  "type": "registry:block",
  "title": "Sign in",
  "description": "A sign in card that morphs from email to a six digit code to your account.",
  "categories": [
    "blocks"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/avatar.json",
    "https://irisui.dev/r/button.json",
    "https://irisui.dev/r/input.json",
    "https://irisui.dev/r/otp-input.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/blocks/sign-in/sign-in.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useLayoutEffect, useId, useRef, useState, type FormEvent } from \"react\";\nimport {\n  AnimatePresence,\n  LayoutGroup,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  type Transition,\n  type Variants,\n} from \"motion/react\";\nimport { Check, KeyRound } from \"lucide-react\";\nimport { Avatar } from \"../../avatar/avatar\";\nimport { Button } from \"../../button/button\";\nimport { Input } from \"../../input/input\";\nimport { OtpInput } from \"../../otp-input/otp-input\";\nimport { motionTokens } from \"../../lib/motion-tokens\";\nimport { cn } from \"../../lib/utils\";\n\ntype Step = \"email\" | \"code\" | \"done\";\ntype Provider = \"Google\" | \"Apple\" | \"GitHub\";\nexport type SignInMethod = \"Email code\" | \"Passkey\" | Provider;\nexport interface SignInAccount {\n  name: string;\n  email: string;\n  photo?: string;\n}\nexport interface SignInProps {\n  /** The code the simulated email contains. */\n  demoCode?: string;\n  /** Called once the simulated sign in succeeds. */\n  onSignIn?: (account: SignInAccount, method: SignInMethod) => void;\n}\ntype StepCustom = { direction: number; reduce: boolean; still?: boolean };\n\n/* Shared class strings. Container steps follow the `sign-in` container at 380 px. */\nconst stepClass = \"grid min-w-0 p-8 @max-[380px]/sign-in:px-5 @max-[380px]/sign-in: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]/sign-in:[&_h2]:text-2xl [&_p]:m-0 [&_p]:text-sm [&_p]:leading-body [&_p]:text-pretty [&_p]:text-ink-2\";\nconst textButtonClass =\n  \"cursor-pointer rounded-[4px] p-0 font-medium text-ink underline decoration-line-strong underline-offset-3 transition-[text-decoration-color] duration-fast ease-standard hover:decoration-current motion-reduce:transition-none\";\nconst avatarMover = \"relative z-1 inline-flex flex-none\";\n\nconst RESEND_SECONDS = 30;\nconst providers: Provider[] = [\"Google\", \"Apple\", \"GitHub\"];\n/** The official four-colour Google \"G\"; Apple and GitHub ship monochrome marks, drawn in currentColor below. */\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 providerMarks: Record<Provider, string> = {\n  Google:\n    \"M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z\",\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};\nconst people: SignInAccount[] = [\n  { name: \"Stelios Antoniou\", email: \"stelios@studio.example\" },\n  { name: \"Katerina Vlachou\", email: \"katerina@studio.example\" },\n  { name: \"Tomasz Wrona\", email: \"tomasz@studio.example\" },\n  { name: \"Nefeli Kontou\", email: \"nefeli@studio.example\" },\n  { name: \"Clara Weiss\", email: \"clara@studio.example\" },\n];\nconst domainFixes: Record<string, string> = {\n  \"gmial.com\": \"gmail.com\",\n  \"gamil.com\": \"gmail.com\",\n  \"gmai.com\": \"gmail.com\",\n  \"gnail.com\": \"gmail.com\",\n  \"gmail.co\": \"gmail.com\",\n  \"hotmial.com\": \"hotmail.com\",\n  \"outlok.com\": \"outlook.com\",\n  \"outlook.co\": \"outlook.com\",\n  \"iclod.com\": \"icloud.com\",\n  \"icoud.com\": \"icloud.com\",\n  \"yaho.com\": \"yahoo.com\",\n};\n\nconst isEmail = (value: string) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/.test(value.trim());\nfunction suggestionFor(value: string) {\n  const [local, domain, extra] = value.trim().toLowerCase().split(\"@\");\n  return local && domain && extra === undefined && domainFixes[domain] ? `${local}@${domainFixes[domain]}` : \"\";\n}\n/** Known teammates keep their full name; anyone else gets a name from their address. Both show an initials avatar. */\nfunction accountFor(value: string): SignInAccount {\n  const email = value.trim().toLowerCase();\n  const parts = email\n    .split(\"@\")[0]!\n    .split(/[._+-]+/)\n    .filter(Boolean);\n  const known = people.find((person) => person.name.toLowerCase().split(\" \")[0] === parts[0]);\n  if (known) return { ...known, email };\n  return {\n    name:\n      parts\n        .slice(0, 2)\n        .map((part) => part[0]!.toUpperCase() + part.slice(1))\n        .join(\" \") || \"CapyDB member\",\n    email,\n  };\n}\n\nconst stepMotion: Variants = {\n  enter: ({ direction, reduce, still }: StepCustom) =>\n    still\n      ? { opacity: 1, x: 0, filter: \"blur(0px)\" }\n      : reduce\n        ? { opacity: 0 }\n        : { opacity: 0, x: direction * 28, 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 * -20,\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};\n/** Seconds roll down while the timer runs and back up when a new code restarts it. */\nconst roll: Variants = {\n  enter: (direction: number) => ({\n    opacity: 0,\n    y: `${-0.7 * direction}em`,\n    filter: `blur(${motionTokens.blur.subtle}px)`,\n  }),\n  center: { opacity: 1, y: 0, filter: \"blur(0px)\" },\n  exit: (direction: number) => ({\n    opacity: 0,\n    y: `${0.7 * direction}em`,\n    filter: `blur(${motionTokens.blur.subtle}px)`,\n  }),\n};\n\nfunction RollingTime({ seconds, reduce }: { seconds: number; reduce: boolean }) {\n  const [shown, setShown] = useState({ seconds, direction: 1 });\n  if (shown.seconds !== seconds) setShown({ seconds, direction: seconds < shown.seconds ? 1 : -1 });\n  const text = `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, \"0\")}`;\n  const transition: Transition = reduce\n    ? { duration: 0 }\n    : {\n        y: motionTokens.spring.snappy,\n        opacity: { duration: motionTokens.duration.fast },\n        filter: { duration: motionTokens.duration.fast },\n      };\n  return (\n    <span className=\"inline-flex tabular-nums\">\n      {text.split(\"\").map((character, index) => (\n        <span key={index} className=\"relative inline-flex justify-center *:inline-block\">\n          <AnimatePresence initial={false} mode=\"popLayout\" custom={shown.direction}>\n            <motion.span\n              key={character}\n              custom={shown.direction}\n              variants={roll}\n              initial=\"enter\"\n              animate=\"center\"\n              exit=\"exit\"\n              transition={transition}\n            >\n              {character}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n      ))}\n    </span>\n  );\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 SignIn({ demoCode = \"123456\", onSignIn }: SignInProps) {\n  const id = useId();\n  const reduce = !!useReducedMotion();\n  const [step, setStep] = useState<Step>(\"email\");\n  const [direction, setDirection] = useState(1);\n  const [email, setEmail] = useState(\"\");\n  const [touched, setTouched] = useState(false);\n  const [attempted, setAttempted] = useState(false);\n  const [code, setCode] = useState(\"\");\n  const [codeError, setCodeError] = useState(\"\");\n  const [resendIn, setResendIn] = useState(RESEND_SECONDS);\n  const [busy, setBusy] = useState<null | \"email\" | \"code\" | \"passkey\" | Provider>(null);\n  const [account, setAccount] = useState<SignInAccount>(people[0]!);\n  const [method, setMethod] = useState<SignInMethod>(\"Email code\");\n  const [status, setStatus] = useState(\"\");\n  const emailRef = useRef<HTMLInputElement>(null);\n  const codeRef = useRef<HTMLDivElement>(null);\n  const doneRef = useRef<HTMLHeadingElement>(null);\n  const focusNext = useRef<\"email\" | \"done\" | null>(null);\n  const timers = useRef<number[]>([]);\n  const { track, height } = useStepHeight(step, reduce);\n\n  const emailError =\n    (touched || attempted) && !isEmail(email)\n      ? email.trim()\n        ? \"Enter a full address, like name@example.com.\"\n        : \"Enter your email address.\"\n      : \"\";\n  const suggestion = touched || attempted ? suggestionFor(email) : \"\";\n  const custom: StepCustom = { direction, reduce };\n  const avatarTransition: Transition = reduce ? { duration: 0 } : motionTokens.spring.morph;\n  const rise = (index: number) => ({\n    initial: reduce ? { opacity: 0 } : { opacity: 0, y: 10, filter: `blur(${motionTokens.blur.soft}px)` },\n    animate: { opacity: 1, y: 0, filter: \"blur(0px)\" },\n    transition: (reduce\n      ? { duration: motionTokens.duration.instant }\n      : {\n          y: { ...motionTokens.spring.smooth, delay: 0.14 + index * motionTokens.stagger.line },\n          opacity: {\n            duration: motionTokens.duration.standard,\n            ease: motionTokens.ease.enter,\n            delay: 0.14 + index * motionTokens.stagger.line,\n          },\n          filter: { duration: motionTokens.duration.standard, delay: 0.14 + index * motionTokens.stagger.line },\n        }) as Transition,\n  });\n\n  useEffect(() => {\n    const pending = timers.current;\n    return () => pending.forEach(window.clearTimeout);\n  }, []);\n  useEffect(() => {\n    if (step !== \"code\" || resendIn <= 0) return;\n    const timer = window.setTimeout(() => setResendIn((seconds) => Math.max(0, seconds - 1)), 1000);\n    return () => window.clearTimeout(timer);\n  }, [step, resendIn]);\n  useEffect(() => {\n    const target = focusNext.current;\n    focusNext.current = null;\n    if (target === \"email\") {\n      emailRef.current?.focus();\n      emailRef.current?.select();\n    }\n    if (target === \"done\") doneRef.current?.focus();\n    if (step === \"code\") codeRef.current?.querySelector(\"input\")?.focus();\n  }, [step]);\n\n  function later(run: () => void, ms: number) {\n    timers.current.push(window.setTimeout(run, ms));\n  }\n  function go(next: Step, towards: number, focus: \"email\" | \"done\" | null = null) {\n    focusNext.current = focus;\n    setDirection(towards);\n    setStep(next);\n  }\n  const focusCode = () => codeRef.current?.querySelector(\"input\")?.focus();\n\n  function complete(next: SignInAccount, how: SignInMethod) {\n    setBusy(null);\n    setAccount(next);\n    setMethod(how);\n    go(\"done\", 1, \"done\");\n    setStatus(`Signed in with ${how === \"Email code\" ? \"an email code\" : how}`);\n    onSignIn?.(next, how);\n  }\n\n  function submitEmail(event: FormEvent<HTMLFormElement>) {\n    event.preventDefault();\n    if (busy) return;\n    setAttempted(true);\n    if (!isEmail(email)) {\n      emailRef.current?.focus();\n      return;\n    }\n    setBusy(\"email\");\n    setStatus(\"Sending a code\");\n    later(() => {\n      setBusy(null);\n      setAccount(accountFor(email));\n      setCode(\"\");\n      setCodeError(\"\");\n      setResendIn(RESEND_SECONDS);\n      go(\"code\", 1);\n      setStatus(\"Code sent\");\n    }, 800);\n  }\n\n  function applySuggestion() {\n    setEmail(suggestion);\n    setStatus(`Email changed to ${suggestion}`);\n    emailRef.current?.focus();\n  }\n\n  function verify(value: string) {\n    if (busy) return;\n    if (value.length < 6) {\n      setCodeError(\"Enter all 6 digits.\");\n      focusCode();\n      return;\n    }\n    setBusy(\"code\");\n    setStatus(\"Checking code\");\n    later(() => {\n      if (value === demoCode) {\n        complete(account, \"Email code\");\n        return;\n      }\n      setBusy(null);\n      setCode(\"\");\n      setCodeError(\"That code didn't match. Check the latest email and try again.\");\n      setStatus(\"Code didn't match\");\n      focusCode();\n    }, 700);\n  }\n\n  function changeCode(value: string) {\n    if (busy) return;\n    setCode(value);\n    if (codeError && value) setCodeError(\"\");\n    if (value.length === 6) verify(value);\n  }\n\n  function resend() {\n    if (busy || resendIn > 0) return;\n    setResendIn(RESEND_SECONDS);\n    setCode(\"\");\n    setCodeError(\"\");\n    setStatus(\"New code sent\");\n    focusCode();\n  }\n\n  function changeEmail() {\n    if (busy) return;\n    setAttempted(false);\n    go(\"email\", -1, \"email\");\n    setStatus(\"Edit your email\");\n  }\n\n  function signInWith(how: \"passkey\" | Provider) {\n    if (busy) return;\n    setBusy(how);\n    setStatus(how === \"passkey\" ? \"Waiting for your passkey\" : `Opening ${how}`);\n    later(() => complete(people[0]!, how === \"passkey\" ? \"Passkey\" : how), how === \"passkey\" ? 1100 : 900);\n  }\n\n  function signOut() {\n    setEmail(account.email);\n    setTouched(false);\n    setAttempted(false);\n    setCode(\"\");\n    go(\"email\", -1, \"email\");\n    setStatus(\"Signed out\");\n  }\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/sign-in [@media(max-width:480px)]:rounded-panel\"\n      aria-label=\"Sign in\"\n    >\n      <LayoutGroup id={id}>\n        <motion.div className=\"overflow-hidden\" style={{ height }}>\n          <div ref={track} className=\"relative\">\n            <AnimatePresence mode=\"popLayout\" initial={false} custom={custom}>\n              {step === \"email\" && (\n                <motion.div\n                  key=\"email\"\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>Sign in to CapyDB</h2>\n                    <p>Enter your email and we will send you a 6 digit code.</p>\n                  </div>\n                  <form className=\"grid gap-4\" onSubmit={submitEmail} noValidate>\n                    <div className=\"grid min-w-0\">\n                      <Input\n                        ref={emailRef}\n                        label=\"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                        readOnly={busy === \"email\"}\n                        error={emailError}\n                        onChange={(event) => setEmail(event.target.value)}\n                        onBlur={() => {\n                          if (email.trim()) setTouched(true);\n                        }}\n                      />\n                      <AnimatePresence initial={false}>\n                        {suggestion && (\n                          <motion.div\n                            key=\"suggestion\"\n                            className=\"overflow-hidden [&_p]:m-0 [&_p]:px-0 [&_p]:pt-2 [&_p]:pb-1 [&_p]:text-xs [&_p]:leading-body [&_p]:[overflow-wrap:anywhere] [&_p]:text-ink-2\"\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                                : {\n                                    height: motionTokens.spring.smooth,\n                                    opacity: { duration: motionTokens.duration.fast },\n                                  }\n                            }\n                          >\n                            <p>\n                              Did you mean{\" \"}\n                              <button\n                                type=\"button\"\n                                className={textButtonClass}\n                                aria-label={`Use ${suggestion}`}\n                                onClick={applySuggestion}\n                              >\n                                {suggestion}\n                              </button>\n                              ?\n                            </p>\n                          </motion.div>\n                        )}\n                      </AnimatePresence>\n                    </div>\n                    <Button type=\"submit\" className=\"w-full\" loading={busy === \"email\"}>\n                      Continue\n                    </Button>\n                  </form>\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                  <div className=\"grid gap-2\">\n                    <Button\n                      type=\"button\"\n                      variant=\"secondary\"\n                      className=\"w-full\"\n                      loading={busy === \"passkey\"}\n                      onClick={() => signInWith(\"passkey\")}\n                    >\n                      <KeyRound size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n                      Sign in with a passkey\n                    </Button>\n                    <fieldset className=\"grid min-w-0 grid-cols-3 gap-2\" aria-label=\"Single sign-on\">\n                      {providers.map((provider) => (\n                        <Button\n                          key={provider}\n                          type=\"button\"\n                          variant=\"secondary\"\n                          className=\"min-w-0 px-2 @max-[380px]/sign-in:gap-1.5 @max-[380px]/sign-in:text-xs\"\n                          aria-label={`Continue with ${provider}`}\n                          loading={busy === provider}\n                          onClick={() => signInWith(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={providerMarks[provider]} />\n                            </svg>\n                          )}\n                          {provider}\n                        </Button>\n                      ))}\n                    </fieldset>\n                  </div>\n                </motion.div>\n              )}\n\n              {step === \"code\" && (\n                <motion.div\n                  key=\"code\"\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>Check your email</h2>\n                    <p>Enter the 6 digit code we sent. It expires in 10 minutes.</p>\n                  </div>\n                  <div className=\"mb-6 flex min-w-0 items-center gap-3 rounded-control border border-line py-2 pr-2 pl-3 [&>button]:flex-none @max-[380px]/sign-in:gap-2 @max-[380px]/sign-in:pl-2\">\n                    <motion.span\n                      layoutId=\"account-avatar\"\n                      className={cn(avatarMover, \"size-8 text-xs/[inherit]\")}\n                      transition={avatarTransition}\n                    >\n                      <Avatar\n                        name={account.name}\n                        src={account.photo}\n                        size=\"xl\"\n                        className=\"size-full text-[length:inherit]\"\n                      />\n                    </motion.span>\n                    <span className=\"min-w-0 flex-1 text-sm leading-body [overflow-wrap:anywhere]\">\n                      {account.email}\n                    </span>\n                    <Button type=\"button\" variant=\"ghost\" size=\"sm\" aria-label=\"Change email\" onClick={changeEmail}>\n                      Change\n                    </Button>\n                  </div>\n                  <form\n                    className=\"grid gap-4\"\n                    onSubmit={(event) => {\n                      event.preventDefault();\n                      verify(code);\n                    }}\n                    noValidate\n                  >\n                    <div ref={codeRef}>\n                      <OtpInput\n                        label=\"Verification code\"\n                        description={`For this demo, the code is ${demoCode}.`}\n                        value={code}\n                        onChange={changeCode}\n                        error={codeError}\n                      />\n                    </div>\n                    <Button type=\"submit\" className=\"w-full\" loading={busy === \"code\"}>\n                      Verify\n                    </Button>\n                  </form>\n                  <button\n                    type=\"button\"\n                    className=\"relative mx-0 mt-3 -mb-2 -ml-2 inline-flex min-h-control-sm cursor-pointer items-center justify-self-start rounded-button px-2 text-sm leading-body font-medium text-ink transition-[color,background-color] duration-fast ease-standard not-aria-disabled:hover:bg-hover aria-disabled:cursor-default aria-disabled:font-normal aria-disabled:text-ink-3 motion-reduce:transition-none\"\n                    aria-disabled={resendIn > 0 || undefined}\n                    aria-label={resendIn > 0 ? `Resend code, available in ${resendIn} seconds` : \"Resend code\"}\n                    onClick={resend}\n                  >\n                    <AnimatePresence mode=\"popLayout\" initial={false}>\n                      <motion.span\n                        key={resendIn > 0 ? \"wait\" : \"ready\"}\n                        className=\"inline-flex items-baseline gap-[.3em] whitespace-nowrap\"\n                        initial={\n                          reduce ? { opacity: 0 } : { opacity: 0, y: 6, filter: `blur(${motionTokens.blur.soft}px)` }\n                        }\n                        animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                        exit={\n                          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                        }\n                        transition={\n                          reduce\n                            ? { duration: motionTokens.duration.instant }\n                            : { duration: motionTokens.duration.standard, ease: motionTokens.ease.enter }\n                        }\n                      >\n                        {resendIn > 0 ? (\n                          <>\n                            Resend code in <RollingTime seconds={resendIn} reduce={reduce} />\n                          </>\n                        ) : (\n                          \"Resend code\"\n                        )}\n                      </motion.span>\n                    </AnimatePresence>\n                  </button>\n                </motion.div>\n              )}\n\n              {step === \"done\" && (\n                <motion.div\n                  key=\"done\"\n                  className={stepClass}\n                  custom={{ ...custom, still: true }}\n                  variants={stepMotion}\n                  initial=\"enter\"\n                  animate=\"center\"\n                  exit=\"exit\"\n                >\n                  <div className=\"relative mt-0 mr-0 mb-6 -ml-1 size-24\">\n                    <svg\n                      className=\"absolute inset-0 size-full overflow-visible text-green [transform:rotate(-90deg)]\"\n                      viewBox=\"0 0 96 96\"\n                      aria-hidden=\"true\"\n                    >\n                      <motion.circle\n                        cx=\"48\"\n                        cy=\"48\"\n                        r=\"47\"\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 * 1.25,\n                            ease: motionTokens.ease.inOut,\n                            delay: 0.22,\n                          },\n                          opacity: { duration: motionTokens.duration.instant, delay: 0.22 },\n                        }}\n                      />\n                    </svg>\n                    <motion.span\n                      layoutId=\"account-avatar\"\n                      className={cn(avatarMover, \"absolute inset-1 text-2xl/[inherit]\")}\n                      initial={method === \"Email code\" || reduce ? false : { opacity: 0, scale: 0.7 }}\n                      animate={{ opacity: 1, scale: 1 }}\n                      transition={avatarTransition}\n                    >\n                      <Avatar\n                        name={account.name}\n                        src={account.photo}\n                        size=\"xl\"\n                        className=\"size-full text-[length:inherit]\"\n                      />\n                    </motion.span>\n                    <motion.span\n                      className=\"absolute right-0.5 bottom-1 z-2 grid size-[26px] place-items-center rounded-full border-2 border-surface bg-green text-surface\"\n                      aria-hidden=\"true\"\n                      initial={reduce ? false : { opacity: 0, scale: 0.4 }}\n                      animate={{ opacity: 1, scale: 1 }}\n                      transition={{ ...motionTokens.spring.snappy, delay: 0.78 }}\n                    >\n                      <Check size={14} strokeWidth={2.25} />\n                    </motion.span>\n                  </div>\n                  <motion.div className={headingClass} {...rise(0)}>\n                    <h2 ref={doneRef} tabIndex={-1}>\n                      Welcome back, {account.name.split(\" \")[0]}\n                    </h2>\n                    <p className=\"[overflow-wrap:anywhere]\">{account.email}</p>\n                  </motion.div>\n                  <motion.dl\n                    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                    {...rise(1)}\n                  >\n                    <div>\n                      <dt>Signed in with</dt>\n                      <dd>{method}</dd>\n                    </div>\n                    <div>\n                      <dt>This browser</dt>\n                      <dd>Remembered for 30 days</dd>\n                    </div>\n                  </motion.dl>\n                  <motion.div {...rise(2)}>\n                    <Button type=\"button\" variant=\"secondary\" className=\"w-full\" onClick={signOut}>\n                      Sign out\n                    </Button>\n                  </motion.div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </div>\n        </motion.div>\n      </LayoutGroup>\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]/sign-in: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\n              key={status}\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={\n                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              }\n              transition={\n                reduce\n                  ? { duration: motionTokens.duration.instant }\n                  : { duration: motionTokens.duration.standard, ease: motionTokens.ease.enter }\n              }\n            >\n              {status}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n        <span className=\"whitespace-nowrap text-ink-2\">\n          No account?{\" \"}\n          <button\n            type=\"button\"\n            className={cn(textButtonClass, \"decoration-transparent\")}\n            onClick={() => setStatus(\"Sign up opens in your app\")}\n          >\n            Sign up\n          </button>\n        </span>\n      </footer>\n    </section>\n  );\n}\n\nexport default SignIn;\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/sign-in/sign-in.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/sign-in",
  "meta": {
    "kind": "block",
    "group": "Blocks",
    "export": "SignIn",
    "docs": "https://irisui.dev/components/sign-in"
  }
}
