"use client"; import { ArrowLeftIcon, CheckIcon, CircleAlertIcon, EyeIcon, EyeOffIcon, type LucideIcon, MailIcon, MailOpenIcon, } from "lucide-react"; import { AnimatePresence, motion, useAnimationControls, useReducedMotion } from "motion/react"; import { type FormEvent, type ReactNode, useEffect, useId, useLayoutEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; import { Spinner } from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; export type SignInMethod = "password" | "link"; export interface SignInProvider { id: string; label: string; icon: LucideIcon; } export type SignInResult = { ok: true; name?: string } | { ok: false; error: string }; export interface SignInCardProps { product?: string; logo?: ReactNode; providers?: SignInProvider[]; defaultEmail?: string; defaultMethod?: SignInMethod; resendAfter?: number; onPasswordSignIn?: (email: string, password: string) => Promise; onSendLink?: (email: string) => Promise; onProvider?: (provider: SignInProvider) => void; onForgotPassword?: (email: string) => void; onCreateAccount?: () => void; className?: string; } const ease = [0.23, 1, 0.32, 1] as const; const press = "transition-[box-shadow,scale,background-color] duration-150 active:scale-[0.97]"; const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; function useMeasuredHeight() { const ref = useRef(null); const [height, setHeight] = useState("auto"); useLayoutEffect(() => { const el = ref.current; if (!el) return; const ro = new ResizeObserver(() => setHeight(el.offsetHeight)); ro.observe(el); return () => ro.disconnect(); }, []); return [ref, height] as const; } function MethodSwitch({ value, onChange }: { value: SignInMethod; onChange: (m: SignInMethod) => void }) { const reduce = useReducedMotion(); const layoutId = useId(); const options: { value: SignInMethod; label: string }[] = [ { value: "password", label: "Password" }, { value: "link", label: "Email link" }, ]; return (
{options.map((o) => { const active = o.value === value; return ( ); })}
); } function FieldMessage({ id, children }: { id: string; children?: string }) { return ( {children && ( {children} )} ); } function formatClock(s: number) { return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; } export function SignInCard({ product = "Larkspur", logo, providers = [], defaultEmail = "", defaultMethod = "password", resendAfter = 30, onPasswordSignIn = async () => ({ ok: true }), onSendLink = async () => {}, onProvider, onForgotPassword, onCreateAccount, className, }: SignInCardProps) { const reduce = useReducedMotion(); const uid = useId(); const [contentRef, height] = useMeasuredHeight(); const shake = useAnimationControls(); const passwordRef = useRef(null); const [view, setView] = useState<"form" | "sent" | "done">("form"); const [method, setMethod] = useState(defaultMethod); const [email, setEmail] = useState(defaultEmail); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); const [busy, setBusy] = useState(false); const [emailError, setEmailError] = useState(); const [passwordError, setPasswordError] = useState(); const [formError, setFormError] = useState(); const [welcome, setWelcome] = useState(); const [cooldown, setCooldown] = useState(0); useEffect(() => { if (cooldown <= 0) return; const t = setTimeout(() => setCooldown((c) => c - 1), 1000); return () => clearTimeout(t); }, [cooldown]); const fail = (message: string) => { setFormError(message); if (!reduce) shake.start({ x: [0, -6, 6, -3, 3, 0], transition: { duration: 0.32, ease: "easeOut" } }); }; const sendLink = async () => { setBusy(true); await onSendLink(email.trim()); setBusy(false); setCooldown(resendAfter); setView("sent"); }; const submit = async (event: FormEvent) => { event.preventDefault(); setFormError(undefined); const trimmed = email.trim(); const badEmail = !trimmed ? "Enter your email address." : !EMAIL.test(trimmed) ? "That doesn't look like an email address." : undefined; const badPassword = method === "password" && !password ? "Enter your password." : undefined; setEmailError(badEmail); setPasswordError(badPassword); if (badEmail || badPassword) { if (!reduce) shake.start({ x: [0, -4, 4, 0], transition: { duration: 0.24, ease: "easeOut" } }); return; } if (method === "link") return sendLink(); setBusy(true); const result = await onPasswordSignIn(trimmed, password); setBusy(false); if (result.ok) { setWelcome(result.name); setView("done"); } else { fail(result.error); setPassword(""); passwordRef.current?.focus(); } }; // `animate` stays identical with reduced motion so server and client render the same styles. const viewMotion = { initial: reduce ? { opacity: 0, y: 0, filter: "blur(0px)" } : { opacity: 0, y: 8, filter: "blur(4px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: reduce ? { opacity: 0, y: 0, filter: "blur(0px)" } : { opacity: 0, y: -8, filter: "blur(4px)" }, }; return (
{view === "form" && (
{logo ?? ( )}

Sign in to {product}

Welcome back. Pick up where you left off.

{providers.length > 0 && ( <>
1 && "grid-cols-2")}> {providers.map((p) => ( ))}
or with email
)} { setMethod(m); setFormError(undefined); setPasswordError(undefined); }} value={method} /> {formError && (

{formError}

)}
{ setEmail(e.target.value); if (emailError) setEmailError(undefined); }} placeholder="you@company.com" type="email" value={email} /> {emailError}
{method === "password" && (
{ setPassword(e.target.value); if (passwordError) setPasswordError(undefined); }} placeholder="Your password" ref={passwordRef} type={showPassword ? "text" : "password"} value={password} /> {passwordError}
)}
{method === "link" && ( We’ll email you a link that signs you in. No password needed. )}
New to {product}?{" "}
)} {view === "sent" && (

Check your inbox

We sent a sign-in link to {email.trim()}. It expires in 15 minutes.

Can’t find it? Check spam, or search for “{product}”.

)} {view === "done" && (

{welcome ? `Welcome back, ${welcome}` : "You're signed in"}

Taking you to your workspace…

)}
); } function Envelope() { const reduce = useReducedMotion(); return (
); }