Retour au catalogue

Event Hero Countdown

Hero 100vh pour evenement avec titre clip-reveal ligne par ligne, countdown a rebours avec effet slot machine (AnimatePresence) sur chaque chiffre, badge date/lieu, CTA bounce et fond aurora radial premium.

eventcomplex Both Responsive a11y
boldelegantluxuryeventeducationagencyuniversalcenteredfullscreen
Theme
"use client";

import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { MapPin, Calendar, ArrowRight } from "lucide-react";

interface EventHeroCountdownProps {
  eventName?: string;
  tagline?: string;
  date?: string;
  location?: string;
  ctaLabel?: string;
  ctaHref?: string;
  targetDate?: string;
}

const EASE = [0.16, 1, 0.3, 1] as const;

interface CountdownState {
  days: number;
  hours: number;
  minutes: number;
}

function useCountdown(target: string): CountdownState {
  const [diff, setDiff] = useState<CountdownState>({ days: 0, hours: 0, minutes: 0 });

  useEffect(() => {
    const calc = () => {
      const ms = Math.max(0, new Date(target).getTime() - Date.now());
      setDiff({
        days: Math.floor(ms / 86400000),
        hours: Math.floor((ms % 86400000) / 3600000),
        minutes: Math.floor((ms % 3600000) / 60000),
      });
    };
    calc();
    const id = setInterval(calc, 60000);
    return () => clearInterval(id);
  }, [target]);

  return diff;
}

function SlotDigit({ value }: { value: string }) {
  return (
    <div style={{ position: "relative", overflow: "hidden", height: "1.15em", display: "inline-block", minWidth: "0.65em" }}>
      <AnimatePresence mode="popLayout" initial={false}>
        <motion.span
          key={value}
          initial={{ y: "100%", opacity: 0 }}
          animate={{ y: "0%", opacity: 1 }}
          exit={{ y: "-100%", opacity: 0 }}
          transition={{ duration: 0.35, ease: EASE }}
          style={{ display: "block", lineHeight: 1.15 }}
        >
          {value}
        </motion.span>
      </AnimatePresence>
    </div>
  );
}

function CountdownNumber({ value, label }: { value: number; label: string }) {
  const str = String(value).padStart(2, "0");
  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "0.5rem" }}>
      <div style={{
        display: "flex",
        fontFamily: "var(--font-sans)",
        fontSize: "clamp(3rem, 8vw, 6rem)",
        fontWeight: 800,
        color: "var(--color-foreground)",
        letterSpacing: "-0.04em",
        lineHeight: 1,
      }}>
        <SlotDigit value={str[0]} />
        <SlotDigit value={str[1]} />
      </div>
      <span style={{
        fontSize: "0.6875rem",
        fontWeight: 600,
        textTransform: "uppercase",
        letterSpacing: "0.1em",
        color: "var(--color-foreground-muted)",
      }}>
        {label}
      </span>
    </div>
  );
}

function TitleWord({ word, delay }: { word: string; delay: number }) {
  return (
    <span style={{ display: "inline-block", overflow: "hidden", verticalAlign: "bottom", marginRight: "0.3em" }}>
      <motion.span
        initial={{ y: "110%" }}
        animate={{ y: "0%" }}
        transition={{ duration: 0.75, delay, ease: EASE }}
        style={{ display: "block" }}
      >
        {word}
      </motion.span>
    </span>
  );
}

export default function EventHeroCountdown({
  eventName = "Design Summit 2026",
  tagline = "The defining event for product designers and founders.",
  date = "September 18, 2026",
  location = "San Francisco, CA",
  ctaLabel = "Register now",
  ctaHref = "#",
  targetDate = "2026-09-18T09:00:00",
}: EventHeroCountdownProps) {
  const countdown = useCountdown(targetDate);
  const words = eventName.split(" ");

  return (
    <section
      style={{
        minHeight: "100vh",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        position: "relative",
        overflow: "hidden",
        background: "var(--color-background)",
      }}
    >
      {/* Aurora background */}
      <div
        aria-hidden
        style={{
          position: "absolute",
          inset: 0,
          background: [
            "radial-gradient(ellipse 80% 60% at 50% -10%, color-mix(in srgb, var(--color-accent) 12%, transparent) 0%, transparent 70%)",
            "radial-gradient(ellipse 50% 40% at 80% 80%, color-mix(in srgb, var(--color-accent) 6%, transparent) 0%, transparent 60%)",
          ].join(", "),
          pointerEvents: "none",
        }}
      />

      <div style={{
        maxWidth: "var(--container-max-width)",
        margin: "0 auto",
        padding: "0 var(--container-padding-x)",
        width: "100%",
        textAlign: "center",
        position: "relative",
        zIndex: 1,
      }}>
        {/* Date + location badges */}
        <motion.div
          initial={{ opacity: 0, y: -8 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5, delay: 0.1, ease: EASE }}
          style={{ display: "flex", justifyContent: "center", gap: "0.75rem", marginBottom: "2rem", flexWrap: "wrap" }}
        >
          {[
            { icon: <Calendar style={{ width: 13, height: 13 }} />, text: date },
            { icon: <MapPin style={{ width: 13, height: 13 }} />, text: location },
          ].map((badge, i) => (
            <span
              key={i}
              style={{
                display: "inline-flex", alignItems: "center", gap: "0.375rem",
                padding: "0.375rem 0.875rem",
                borderRadius: "var(--radius-full)",
                border: "1px solid var(--color-border)",
                background: "var(--color-background-card)",
                fontSize: "0.8125rem",
                color: "var(--color-foreground-muted)",
                fontWeight: 500,
              }}
            >
              {badge.icon}
              {badge.text}
            </span>
          ))}
        </motion.div>

        {/* Title clip-reveal */}
        <h1
          style={{
            fontFamily: "var(--font-sans)",
            fontSize: "clamp(2.5rem, 7vw, 5.5rem)",
            fontWeight: 800,
            color: "var(--color-foreground)",
            letterSpacing: "-0.03em",
            lineHeight: 1.05,
            marginBottom: "1.25rem",
          }}
        >
          {words.map((word, i) => (
            <TitleWord key={i} word={word} delay={0.25 + i * 0.1} />
          ))}
        </h1>

        {/* Tagline */}
        <motion.p
          initial={{ opacity: 0, y: 10 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.55, delay: 0.5 + words.length * 0.1, ease: EASE }}
          style={{
            fontSize: "clamp(1rem, 2vw, 1.1875rem)",
            color: "var(--color-foreground-muted)",
            maxWidth: "520px",
            margin: "0 auto 3.5rem",
            lineHeight: 1.65,
          }}
        >
          {tagline}
        </motion.p>

        {/* Countdown */}
        <motion.div
          initial={{ opacity: 0, scale: 0.96 }}
          animate={{ opacity: 1, scale: 1 }}
          transition={{ duration: 0.5, delay: 0.65, ease: EASE }}
          style={{
            display: "flex",
            justifyContent: "center",
            alignItems: "flex-start",
            gap: "clamp(1.5rem, 5vw, 4rem)",
            marginBottom: "3.5rem",
          }}
        >
          <CountdownNumber value={countdown.days} label="Days" />
          <div style={{ fontSize: "clamp(2rem, 5vw, 4rem)", fontWeight: 800, color: "var(--color-border)", lineHeight: 1, paddingTop: "0.1em" }}>:</div>
          <CountdownNumber value={countdown.hours} label="Hours" />
          <div style={{ fontSize: "clamp(2rem, 5vw, 4rem)", fontWeight: 800, color: "var(--color-border)", lineHeight: 1, paddingTop: "0.1em" }}>:</div>
          <CountdownNumber value={countdown.minutes} label="Minutes" />
        </motion.div>

        {/* CTA */}
        <motion.a
          href={ctaHref}
          initial={{ opacity: 0, y: 16 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.45, delay: 0.8, ease: EASE }}
          whileHover={{ scale: 1.04 }}
          whileTap={{ scale: 0.97 }}
          style={{
            display: "inline-flex",
            alignItems: "center",
            gap: "0.5rem",
            padding: "0.9375rem 2.25rem",
            borderRadius: "var(--radius-full)",
            background: "var(--color-foreground)",
            color: "var(--color-background)",
            fontWeight: 700,
            fontSize: "0.9375rem",
            textDecoration: "none",
            letterSpacing: "-0.01em",
            cursor: "pointer",
          }}
        >
          {ctaLabel}
          <motion.span
            animate={{ x: [0, 4, 0] }}
            transition={{ duration: 1.4, repeat: Infinity, ease: "easeInOut", repeatDelay: 0.6 }}
            style={{ display: "flex" }}
          >
            <ArrowRight style={{ width: 16, height: 16 }} />
          </motion.span>
        </motion.a>
      </div>
    </section>
  );
}

Avis

Event Hero Countdown — React Event Section — Incubator