"use client"; import { ArrowRightIcon, CheckIcon, CircleAlertIcon, EyeIcon, EyeOffIcon, MailCheckIcon, TrendingUpIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type FormEvent, type ReactNode, useEffect, useId, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; import { Spinner } from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; export interface SignUpQuote { quote: string; name: string; role: string; tone?: string; } export interface SignUpValues { name: string; email: string; password: string; } export interface SplitSignUpProps { product?: string; title?: string; subtitle?: string; quotes?: SignUpQuote[]; showcase?: ReactNode; checkEmail?: (email: string) => Promise; onSubmit?: (values: SignUpValues) => Promise; onSignIn?: () => 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,}$/; const RULES = [ { id: "len", label: "8+ characters", test: (p: string) => p.length >= 8 }, { id: "num", label: "A number", test: (p: string) => /\d/.test(p) }, { id: "mix", label: "Upper & lower case", test: (p: string) => /[a-z]/.test(p) && /[A-Z]/.test(p), }, ]; const STRENGTH = [ { label: "Too short", bar: "bg-destructive", text: "text-destructive-foreground", }, { label: "Weak", bar: "bg-destructive", text: "text-destructive-foreground" }, { label: "Fair", bar: "bg-warning", text: "text-warning-foreground" }, { label: "Good", bar: "bg-info", text: "text-info-foreground" }, { label: "Strong", bar: "bg-success", text: "text-success-foreground" }, ]; function strengthOf(p: string) { if (!p) return 0; let s = RULES.filter((r) => r.test(p)).length; if (p.length >= 12 || /[^A-Za-z0-9]/.test(p)) s += 1; return p.length < 8 ? Math.min(s, 1) : Math.max(1, s); } function Message({ id, tone = "error", children }: { id: string; tone?: "error" | "muted"; children?: ReactNode }) { return ( {children && (

{children}

)}
); } function StatusIcon({ state }: { state: "idle" | "checking" | "valid" }) { return ( {state !== "idle" && ( {state === "checking" ? ( ) : ( )} )} ); } function DefaultShowcase() { const reduce = useReducedMotion(); const bars = [38, 52, 44, 61, 58, 72, 84]; return (

Weekly bookings

18%

1,284

{bars.map((h, i) => ( ))}
Mon Sun
JR Jonah booked a table Friday, 7:30 pm ยท party of 4
); } function QuoteBody({ item, className, ...rest }: { item: SignUpQuote; className?: string; "aria-hidden"?: boolean }) { const initials = item.name .split(" ") .map((w) => w[0]) .join("") .slice(0, 2); return (
โ€œ{item.quote}โ€
{initials} {item.name} {item.role}
); } function Quotes({ quotes }: { quotes: SignUpQuote[] }) { const reduce = useReducedMotion(); const [index, setIndex] = useState(0); const [paused, setPaused] = useState(false); useEffect(() => { if (paused || quotes.length < 2) return; const t = setTimeout(() => setIndex((i) => (i + 1) % quotes.length), 7000); return () => clearTimeout(t); }, [index, paused, quotes.length]); const q = quotes[index]; if (!q) return null; return (
setPaused(false)} onFocus={() => setPaused(true)} onPointerEnter={() => setPaused(true)} onPointerLeave={() => setPaused(false)} >
{quotes.map((item) => ( ))}
{quotes.length > 1 && (
{quotes.map((item, i) => ( ))}
)}
); } export function SplitSignUp({ product = "Tidewell", title = "Create your account", subtitle = "Start a 14-day trial. No card needed.", quotes = [], showcase, checkEmail, onSubmit = async () => {}, onSignIn, className, }: SplitSignUpProps) { const reduce = useReducedMotion(); const uid = useId(); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [agree, setAgree] = useState(false); const [showPassword, setShowPassword] = useState(false); const [touched, setTouched] = useState>({}); const [emailTaken, setEmailTaken] = useState(false); const [checking, setChecking] = useState(false); const [submitted, setSubmitted] = useState(false); const [busy, setBusy] = useState(false); const [done, setDone] = useState(false); const checkSeq = useRef(0); const strength = strengthOf(password); const passed = RULES.every((r) => r.test(password)); const show = (field: string) => touched[field] || submitted; const nameError = show("name") && !name.trim() ? "Tell us what to call you." : undefined; const emailFormatError = !email.trim() ? "Enter your work email." : !EMAIL.test(email.trim()) ? "That doesn't look like an email address." : undefined; const emailError = show("email") ? emailFormatError : undefined; const passwordError = show("password") && !passed ? "Your password needs every item below." : undefined; const agreeError = submitted && !agree ? "Please accept the terms to continue." : undefined; const runEmailCheck = async () => { setTouched((t) => ({ ...t, email: true })); if (!checkEmail || emailFormatError) return; const seq = ++checkSeq.current; setChecking(true); const taken = await checkEmail(email.trim()); if (seq !== checkSeq.current) return; setChecking(false); setEmailTaken(taken); }; const submit = async (event: FormEvent) => { event.preventDefault(); setSubmitted(true); if (!name.trim() || emailFormatError || emailTaken || !passed || !agree) return; setBusy(true); await onSubmit({ name: name.trim(), email: email.trim(), password }); setBusy(false); setDone(true); }; const emailState = checking ? "checking" : !emailFormatError && touched.email && !emailTaken ? "valid" : "idle"; return (
{product}

{title}

{subtitle}

setTouched((t) => ({ ...t, name: true }))} onChange={(e) => setName(e.target.value)} placeholder="Ada Lindqvist" value={name} /> {nameError}
{ setEmail(e.target.value); setEmailTaken(false); checkSeq.current++; setChecking(false); }} placeholder="ada@company.com" type="email" value={email} /> {emailError ?? (emailTaken ? ( <> There's already an account for this email.{" "} ) : undefined)}
{password ? STRENGTH[strength].label : "โ€“"}
setTouched((t) => ({ ...t, password: true }))} onChange={(e) => setPassword(e.target.value)} placeholder="Create a password" type={showPassword ? "text" : "password"} value={password} />
{[1, 2, 3, 4].map((step) => ( = step ? 1 : 0 }} className={cn("block h-full origin-left rounded-full transition-colors duration-200", STRENGTH[strength].bar)} initial={false} transition={reduce ? { duration: 0 } : { duration: 0.24, ease }} /> ))}
    {RULES.map((rule) => { const ok = rule.test(password); const bad = !ok && show("password"); return (
  • {ok ? "Done: " : "Missing: "} {rule.label}
  • ); })}
{agreeError}
{submitted && (nameError || emailError || emailTaken || passwordError || agreeError) && ( Fix the highlighted fields to continue. )}

Already have an account?{" "}

{done && (

Check your email, {name.trim().split(" ")[0]}

We sent a confirmation link to {email.trim()}. Open it to finish setting up your workspace.

)}
); }