# Auth Magic Link Sent — React Auth Screen

> A check-your-inbox screen: an envelope opens and its letter lifts on a loop, a ring counts down to when resend unlocks, and the address can be changed in place.

- Category: [Auth Screens](https://pixelpapa.com/components/auth.md)
- Tags: auth, magic-link, email, countdown, resend
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/auth/auth-magic-link-sent

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/auth-magic-link-sent.json"
```

The file is written to `components/pixelpapa/auth-magic-link-sent.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { ArrowRight, Check, Pencil } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useState, type FormEvent } from "react";

function Envelope() {
  const reduce = useReducedMotion();
  const loop = { duration: 3.6, repeat: Infinity, ease: "easeInOut" as const };
  return (
    <svg viewBox="0 0 160 130" className="mx-auto w-40 overflow-visible" aria-hidden>
      <ellipse cx="80" cy="122" rx="46" ry="5" className="fill-foreground/10" />
      <rect x="24" y="48" width="112" height="68" rx="10" className="fill-primary/25" />
      <motion.g animate={reduce ? undefined : { y: [0, -22, -22, 0] }} transition={{ ...loop, times: [0, 0.35, 0.75, 1] }}>
        <rect x="38" y="34" width="84" height="66" rx="6" className="fill-card stroke-border" strokeWidth="1.5" />
        <rect x="50" y="48" width="38" height="5" rx="2.5" className="fill-primary" />
        <rect x="50" y="60" width="60" height="4" rx="2" className="fill-muted-foreground/30" />
        <rect x="50" y="70" width="48" height="4" rx="2" className="fill-muted-foreground/30" />
      </motion.g>
      <path d="M24 60v46a10 10 0 0 0 10 10h92a10 10 0 0 0 10-10V60L80 94Z" className="fill-primary" />
      <path d="M24 106 62 82M136 106 98 82" className="stroke-primary-foreground/40" strokeWidth="1.5" strokeLinecap="round" />
      {[
        { x: 18, y: 30, d: 0 },
        { x: 140, y: 22, d: 1.1 },
        { x: 128, y: 4, d: 2 },
      ].map((s) => (
        <motion.path
          key={s.x}
          d={`M${s.x} ${s.y - 6}v12M${s.x - 6} ${s.y}h12`}
          className="stroke-primary"
          strokeWidth="2"
          strokeLinecap="round"
          style={{ transformOrigin: `${s.x}px ${s.y}px` }}
          animate={reduce ? undefined : { scale: [0, 1, 0], rotate: [0, 45, 90] }}
          transition={{ duration: 2.2, repeat: Infinity, delay: s.d }}
        />
      ))}
    </svg>
  );
}

export default function AuthMagicLinkSent({ defaultEmail = "wren@littleatlas.co", cooldown = 30 }: { defaultEmail?: string; cooldown?: number }) {
  const [email, setEmail] = useState(defaultEmail);
  const [draft, setDraft] = useState(defaultEmail);
  const [editing, setEditing] = useState(false);
  const [seconds, setSeconds] = useState(cooldown);
  const [resent, setResent] = useState(0);

  useEffect(() => {
    if (seconds <= 0 || editing) return;
    const id = setTimeout(() => setSeconds((s) => s - 1), 1000);
    return () => clearTimeout(id);
  }, [seconds, editing]);

  function resend() {
    setSeconds(cooldown);
    setResent((n) => n + 1);
  }

  function onSubmit(e: FormEvent) {
    e.preventDefault();
    setEmail(draft);
    setEditing(false);
    resend();
  }

  const C = 2 * Math.PI * 9;

  return (
    <section className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-background px-6 py-16">
      <div aria-hidden className="absolute top-1/2 left-1/2 size-[560px] -translate-x-1/2 -translate-y-[60%] rounded-full bg-primary/10 blur-[110px]" />
      <div className="relative w-full max-w-md text-center">
        <Envelope />
        <AnimatePresence mode="wait" initial={false}>
          {editing ? (
            <motion.form key="edit" onSubmit={onSubmit} initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -12 }} className="mt-8">
              <h1 className="text-3xl font-semibold tracking-tight text-foreground">Where should we send it?</h1>
              <label htmlFor="auth-magic-link-sent-email" className="sr-only">
                Email address
              </label>
              <div className="mt-6 flex gap-2">
                <input
                  id="auth-magic-link-sent-email"
                  type="email"
                  required
                  autoFocus
                  value={draft}
                  onChange={(e) => setDraft(e.target.value)}
                  className="h-12 min-w-0 flex-1 rounded-xl border border-input bg-card px-4 text-sm text-foreground outline-none focus:border-primary focus:ring-4 focus:ring-primary/15"
                />
                <button type="submit" className="inline-flex h-12 items-center gap-2 rounded-xl bg-primary px-5 text-sm font-medium text-primary-foreground">
                  Send link <ArrowRight className="size-4" />
                </button>
              </div>
            </motion.form>
          ) : (
            <motion.div key="sent" initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -12 }} className="mt-8">
              <h1 className="text-3xl font-semibold tracking-tight text-foreground">Check your inbox</h1>
              <p className="mt-3 text-muted-foreground">We sent a sign-in link to</p>
              <button
                type="button"
                onClick={() => setEditing(true)}
                className="group mt-2 inline-flex items-center gap-2 rounded-full border border-border bg-card py-1.5 pr-3 pl-4 text-sm font-medium text-foreground transition-colors hover:border-primary/50"
              >
                {email}
                <Pencil className="size-3.5 text-muted-foreground transition-colors group-hover:text-primary" />
                <span className="sr-only">Change email address</span>
              </button>
              <p className="mx-auto mt-5 max-w-xs text-sm text-muted-foreground">
                The link works once and expires in 15 minutes. It will open Little Atlas on this device.
              </p>

              <div className="mt-8 flex items-center justify-center gap-3 border-t border-border pt-6 text-sm">
                <button
                  type="button"
                  onClick={resend}
                  disabled={seconds > 0}
                  className="inline-flex h-10 items-center gap-2.5 rounded-full border border-border bg-card px-4 font-medium text-foreground transition-colors hover:bg-muted disabled:text-muted-foreground disabled:hover:bg-card"
                >
                  <svg viewBox="0 0 24 24" className="size-5 -rotate-90" aria-hidden>
                    <circle cx="12" cy="12" r="9" fill="none" strokeWidth="2.5" className="stroke-border" />
                    <circle
                      cx="12" cy="12" r="9" fill="none" strokeWidth="2.5" strokeLinecap="round"
                      className="stroke-primary transition-[stroke-dashoffset] duration-1000 ease-linear"
                      strokeDasharray={C}
                      strokeDashoffset={C * (seconds / cooldown)}
                    />
                  </svg>
                  {seconds > 0 ? <span className="tabular-nums">Resend in {seconds}s</span> : "Resend link"}
                </button>
                <AnimatePresence>
                  {resent > 0 && seconds > cooldown - 4 && (
                    <motion.span
                      key={resent}
                      initial={{ opacity: 0, x: -8 }}
                      animate={{ opacity: 1, x: 0 }}
                      exit={{ opacity: 0 }}
                      className="inline-flex items-center gap-1.5 text-emerald-600 dark:text-emerald-400"
                    >
                      <Check className="size-4" strokeWidth={3} /> Sent again
                    </motion.span>
                  )}
                </AnimatePresence>
              </div>
              <p className="mt-5 text-xs text-muted-foreground">
                Nothing yet? Look in spam, or <a href="#" className="underline underline-offset-2 hover:text-foreground">sign in with a password</a>.
              </p>
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "Auth Magic Link Sent" component from PixelPapa UI to this project.

What it is: A check-your-inbox screen: an envelope opens and its letter lifts on a loop, a ring counts down to when resend unlocks, and the address can be changed in place.
Category: Auth Screens. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/auth-magic-link-sent.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/auth-magic-link-sent.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/auth-magic-link-sent" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More auth screens

- [Auth Card Social](https://pixelpapa.com/components/auth/auth-card-social.md): A centred sign-in card with generic provider buttons that show their own loading state, and an email step that unfolds a password field once the address is valid.
- [Auth OTP Verify](https://pixelpapa.com/components/auth/auth-otp-verify.md) (Pro): A six-box verification screen with auto-advance, backspace, arrow keys and paste; digits pop in, a wrong code shakes and clears, the right one ripples green.
- [Auth Password Strength](https://pixelpapa.com/components/auth/auth-password-strength.md): A set-password screen with a four-segment meter that fills and recolours as you type, a requirements checklist that ticks off live and a one-click strong suggestion.
- [Auth Signup Steps](https://pixelpapa.com/components/auth/auth-signup-steps.md) (Pro): A three-step sign-up card with a filling progress rail, steps that slide in the direction of travel, a live workspace URL preview and a celebratory finish.
- [Auth Split Brand Panel](https://pixelpapa.com/components/auth/auth-split-brand-panel.md) (Pro): Sign-in form on one side and a brand panel on the other, where collaborators orbit a logo on slow rings and customer quotes rotate on a timer.
