"use client"; import { AlertCircleIcon, ArrowRightIcon, LockIcon } from "lucide-react"; import { AnimatePresence, motion, MotionConfig, useAnimationControls, useReducedMotion } from "motion/react"; import { type FormEvent, type ReactNode, useEffect, useId, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; function useReducedMotionSafe() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && Boolean(prefers); } export interface NewsletterIssue { number: number; title: string; date: string; minutes: number; } export interface NewsletterCtaProps { eyebrow?: string; title: ReactNode; description?: ReactNode; subscribers: number; readers?: string[]; issues?: NewsletterIssue[]; note?: string; onSubscribe?: (email: string) => Promise | void; className?: string; } const ease = [0.23, 1, 0.32, 1] as const; const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; const tones = [ "bg-rose-100 text-rose-700 dark:bg-rose-950 dark:text-rose-300", "bg-sky-100 text-sky-700 dark:bg-sky-950 dark:text-sky-300", "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300", "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300", "bg-violet-100 text-violet-700 dark:bg-violet-950 dark:text-violet-300", ]; const typos: Record = { "gmial.com": "gmail.com", "gmal.com": "gmail.com", "gmail.co": "gmail.com", "gnail.com": "gmail.com", "hotmial.com": "hotmail.com", "hotmail.co": "hotmail.com", "yaho.com": "yahoo.com", "outlok.com": "outlook.com", "icloud.co": "icloud.com", "iclod.com": "icloud.com", }; function suggest(email: string) { const [user, domain] = email.toLowerCase().split("@"); if (!user || !domain) return null; const fixed = typos[domain]; return fixed ? `${user}@${fixed}` : null; } function RollingDigit({ digit }: { digit: number }) { const reduce = useReducedMotionSafe(); return ( {Array.from({ length: 10 }, (_, n) => ( {n} ))} ); } function Count({ value }: { value: number }) { const text = value.toLocaleString("en-US"); const chars = text.split(""); return ( {text} {chars.map((char, i) => { const key = chars.length - i; return /\d/.test(char) ? ( ) : ( {char} ); })} ); } function initialsFrom(email: string) { const name = email.split("@")[0].replace(/[^a-z]/gi, " ").trim(); const parts = name.split(/\s+/).filter(Boolean); return ((parts[0]?.[0] ?? "Y") + (parts[1]?.[0] ?? parts[0]?.[1] ?? "")).toUpperCase(); } function IssueStack({ issues }: { issues: NewsletterIssue[] }) { const reduce = useReducedMotionSafe(); const [open, setOpen] = useState(false); const shown = issues.slice(0, 3); return (
e.pointerType === "mouse" && setOpen(true)} onPointerLeave={() => setOpen(false)} > {shown.map((issue, i) => { const depth = shown.length - 1 - i; const rest = { y: depth * -18, scale: 1 - depth * 0.05, rotate: depth === 0 ? -2 : depth === 1 ? 2 : -1 }; const fanned = { y: depth * -46, scale: 1 - depth * 0.03, rotate: depth === 0 ? -2 : depth === 1 ? 1.5 : -1 }; return ( 0 && "pointer-events-none", )} initial={false} key={issue.number} style={{ zIndex: i }} transition={{ type: "spring", stiffness: 300, damping: 30 }} >
0 && !open && "opacity-0", )} > No. {issue.number} {issue.date}

0 && "opacity-0")}> {issue.title}

0 && "opacity-0")} aria-hidden>

0 && "opacity-0")}> {issue.minutes} min read

); })}
); } export function NewsletterCta({ eyebrow, title, description, subscribers, readers = [], issues = [], note = "No spam. One click to unsubscribe.", onSubscribe, className, }: NewsletterCtaProps) { const reduce = useReducedMotionSafe(); const fieldId = useId(); const errorId = useId(); const inputRef = useRef(null); const shake = useAnimationControls(); const [email, setEmail] = useState(""); const [error, setError] = useState(null); const [status, setStatus] = useState<"idle" | "sending" | "done">("idle"); const [count, setCount] = useState(subscribers); const [joined, setJoined] = useState(null); const hint = error ? null : suggest(email); useEffect(() => setCount(subscribers), [subscribers]); const fail = (message: string) => { setError(message); if (!reduce) shake.start({ x: [0, -6, 5, -3, 2, 0], transition: { duration: 0.32, ease: "easeOut" } }); inputRef.current?.focus(); }; const submit = async (event: FormEvent) => { event.preventDefault(); const value = email.trim(); if (!value) return fail("Add your email so we know where to send it."); if (!EMAIL.test(value)) return fail("That doesn't look like an email address yet."); setError(null); setStatus("sending"); try { await Promise.all([onSubscribe?.(value), new Promise((r) => setTimeout(r, 900))]); setJoined(initialsFrom(value)); setCount((c) => c + 1); setStatus("done"); } catch (err) { setStatus("idle"); fail(err instanceof Error ? err.message : "Something went wrong. Try again in a moment."); } }; const undo = () => { setStatus("idle"); setJoined(null); setCount((c) => c - 1); setEmail(""); requestAnimationFrame(() => inputRef.current?.focus()); }; const stack = joined ? [...readers.slice(0, 4), joined] : readers.slice(0, 4); return (
{stack.map((initials, i) => ( {initials} ))}

{joined ? "You're reader no." : "Join"} {!joined && "readers"}

{eyebrow && (

{eyebrow}

)}

{title}

{description && (

{description}

)}
{status !== "done" ? ( { const v = email.trim(); if (v && !EMAIL.test(v)) setError("That doesn't look like an email address yet."); }} onChange={(e) => { setEmail(e.target.value); if (error && EMAIL.test(e.target.value.trim())) setError(null); }} placeholder="you@company.com" ref={inputRef} type="email" value={email} />
{error ? ( {error} ) : hint ? ( Did you mean{" "} ? ) : ( {note} )}
) : (

You're on the list.

We sent a confirmation to {email.trim()}. The next issue lands Thursday.

)}
{issues.length > 0 && ( )}
); }