"use client"; import { useEffect, useId, useLayoutEffect, useRef, useState, type CSSProperties, type FormEvent, type PointerEvent, type ReactNode } from "react"; import "./liquid-glass-cta.css"; /** * Liquid Glass CTA * A newsletter sign-up held in a capsule of glass floating over slow colour. * The capsule bends the colour behind it at its rim and catches the light * where your pointer is. Sign up and it squashes like a drop, then settles * into a single line that says you're in. */ type Props = { eyebrow?: string; headline: ReactNode; sub?: ReactNode; action?: string; /** Resolve to sign up; reject to show an error. Defaults to a short wait. */ onSubmit?: (email: string) => Promise; done?: (email: string) => ReactNode; palette?: "dusk" | "lagoon"; motion?: "full" | "reduced"; className?: string; }; const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; const BEZEL = 16; /** Neutral grey in the middle, ramping to full red/blue offsets across the rim. */ function lensMap(w: number, h: number) { const r = h / 2; const svg = ``; return `data:image/svg+xml,${encodeURIComponent(svg)}`; } export function LiquidGlassCta({ eyebrow, headline, sub, action = "Subscribe", onSubmit, done, palette = "dusk", motion = "full", className = "" }: Props) { const uid = useId().replace(/:/g, ""); const glass = useRef(null); const input = useRef(null); const [box, setBox] = useState({ w: 0, h: 0 }); const [refract, setRefract] = useState(false); const [email, setEmail] = useState(""); const [error, setError] = useState(""); const [phase, setPhase] = useState<"idle" | "sending" | "done">("idle"); useEffect(() => { const chromium = /Chrome\//.test(navigator.userAgent) && !/Edg\/|OPR\//.test(navigator.userAgent); setRefract(chromium && CSS.supports("backdrop-filter", "url(#x) blur(1px)")); }, []); useLayoutEffect(() => { const el = glass.current!; const read = () => setBox({ w: el.offsetWidth, h: el.offsetHeight }); read(); const ro = new ResizeObserver(read); ro.observe(el); return () => ro.disconnect(); }, []); const move = (e: PointerEvent) => { const el = glass.current!, r = el.getBoundingClientRect(); el.style.setProperty("--mx", `${(((e.clientX - r.left) / r.width) * 100).toFixed(1)}%`); el.style.setProperty("--my", `${(((e.clientY - r.top) / r.height) * 100).toFixed(1)}%`); }; const squish = () => { const el = glass.current!; el.classList.remove("lgcta__glass--squish"); void el.offsetWidth; el.classList.add("lgcta__glass--squish"); }; const submit = async (e: FormEvent) => { e.preventDefault(); if (phase !== "idle") return; const v = email.trim(); if (!v) { setError("Add your email and we'll send the first letter this Sunday."); input.current?.focus(); return; } if (!EMAIL.test(v)) { setError("That doesn't look like an email address. Check for a missing @ or dot."); input.current?.focus(); return; } setError(""); setPhase("sending"); try { await (onSubmit ? onSubmit(v) : new Promise((r) => setTimeout(r, 700))); squish(); setPhase("done"); } catch { setPhase("idle"); setError("We couldn't sign you up just now. Your email is still here; try again in a moment."); } }; const lens = refract && box.w > 0; return (
); }