Retour au catalogue
Waitlist Countdown Combo
Inscription waitlist combinee avec un compte a rebours vers la date de lancement. Timer anime et formulaire email.
waitlistmedium Both Responsive a11y
bolddarksaasuniversalagencycentered
Theme
"use client";
import { useRef, useState, useEffect } from "react";
import { motion, useInView } from "framer-motion";
import { Clock, ArrowRight, Sparkles } from "lucide-react";
interface TimerLabels {
days: string;
hours: string;
minutes: string;
seconds: string;
}
interface WaitlistCountdownComboProps {
title?: string;
subtitle?: string;
ctaLabel?: string;
placeholder?: string;
badge?: string;
targetDate?: string;
timerLabels?: TimerLabels;
note?: string;
}
const EASE: [number, number, number, number] = [0.16, 1, 0.3, 1];
function useCountdown(target: string) {
const [time, setTime] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 });
useEffect(() => {
const calc = () => {
const diff = Math.max(0, new Date(target).getTime() - Date.now());
setTime({
days: Math.floor(diff / 86400000),
hours: Math.floor((diff % 86400000) / 3600000),
minutes: Math.floor((diff % 3600000) / 60000),
seconds: Math.floor((diff % 60000) / 1000),
});
};
calc();
const id = setInterval(calc, 1000);
return () => clearInterval(id);
}, [target]);
return time;
}
export default function WaitlistCountdownCombo({
title = "Le lancement approche",
subtitle = "Reservez votre place avant l'ouverture officielle.",
ctaLabel = "Me prevenir",
placeholder = "votre@email.com",
badge = "Lancement imminent",
targetDate = "2026-06-15T00:00:00",
timerLabels = { days: "Jours", hours: "Heures", minutes: "Minutes", seconds: "Secondes" },
note,
}: WaitlistCountdownComboProps) {
const ref = useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, margin: "-80px" });
const time = useCountdown(targetDate);
const units: (keyof typeof time)[] = ["days", "hours", "minutes", "seconds"];
return (
<section style={{ padding: "var(--section-padding-y) 0", background: "var(--color-background)", position: "relative", overflow: "hidden" }}>
<div ref={ref} style={{ maxWidth: 640, margin: "0 auto", padding: "0 var(--container-padding-x)", textAlign: "center", position: "relative", zIndex: 1 }}>
<motion.div initial={{ opacity: 0, y: 20 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.5, ease: EASE }}>
{badge && (
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "0.4rem 1rem", borderRadius: "var(--radius-full)", border: "1px solid var(--color-border)", background: "var(--color-background-card)", fontSize: "0.8125rem", fontWeight: 500, color: "var(--color-foreground-muted)", marginBottom: "1.25rem" }}>
<Clock style={{ width: 14, height: 14, color: "var(--color-accent)" }} />
{badge}
</span>
)}
<h2 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(1.5rem, 3vw, 2.5rem)", fontWeight: 700, lineHeight: 1.15, letterSpacing: "-0.02em", color: "var(--color-foreground)", marginBottom: "0.75rem" }}>{title}</h2>
<p style={{ fontSize: "1rem", lineHeight: 1.65, color: "var(--color-foreground-muted)", marginBottom: "2.5rem" }}>{subtitle}</p>
</motion.div>
{/* Countdown timer */}
<motion.div initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.1, ease: EASE }} style={{ display: "flex", justifyContent: "center", gap: "1rem", marginBottom: "2.5rem", flexWrap: "wrap" }}>
{units.map((unit, i) => (
<motion.div key={unit} initial={{ opacity: 0, scale: 0.9 }} animate={inView ? { opacity: 1, scale: 1 } : {}} transition={{ duration: 0.4, delay: 0.15 + i * 0.06, ease: EASE }} style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: "1.25rem 1.5rem", borderRadius: "var(--radius-lg)", border: "1px solid var(--color-border)", background: "var(--color-background-card)", minWidth: 90 }}>
<span style={{ fontSize: "2rem", fontWeight: 800, fontFamily: "var(--font-sans)", color: "var(--color-foreground)", lineHeight: 1, fontVariantNumeric: "tabular-nums" }}>{String(time[unit]).padStart(2, "0")}</span>
<span style={{ fontSize: "0.6875rem", fontWeight: 500, textTransform: "uppercase", letterSpacing: "0.1em", color: "var(--color-foreground-muted)", marginTop: "0.375rem" }}>{timerLabels[unit]}</span>
</motion.div>
))}
</motion.div>
{/* Email form */}
<motion.div initial={{ opacity: 0, y: 12 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.5, delay: 0.25, ease: EASE }} style={{ display: "flex", gap: 8, maxWidth: 440, margin: "0 auto" }}>
<input type="email" placeholder={placeholder} style={{ flex: 1, padding: "0.875rem 1.25rem", borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-background-card)", color: "var(--color-foreground)", fontSize: "0.9375rem", outline: "none", minWidth: 0 }} />
<button style={{ padding: "0.875rem 1.5rem", borderRadius: "var(--radius-md)", background: "var(--color-accent)", color: "var(--color-foreground)", fontWeight: 600, fontSize: "0.9375rem", border: "none", cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }}>
{ctaLabel} <ArrowRight style={{ width: 16, height: 16 }} />
</button>
</motion.div>
{note && (
<motion.p initial={{ opacity: 0 }} animate={inView ? { opacity: 1 } : {}} transition={{ duration: 0.4, delay: 0.35 }} style={{ fontSize: "0.8125rem", color: "var(--color-foreground-muted)", marginTop: "1.25rem", display: "flex", alignItems: "center", justifyContent: "center", gap: 6 }}>
<Sparkles style={{ width: 13, height: 13, color: "var(--color-accent)" }} />
{note}
</motion.p>
)}
</div>
</section>
);
}