Fazekit

Code

"use client";
import * as React from "react";
import { motion, useReducedMotion, type MotionProps } from "motion/react";
import { ArrowRight, Check, Play, Sparkles, TrendingUp } from "lucide-react";
import { cn } from "@/lib/utils";

export interface HeroAuroraLink {
  label: string;
  href?: string;
}

export interface HeroAuroraProps {
  /** Small pill above the headline. Pass `null` to hide it. */
  announcement?: (HeroAuroraLink & { tag?: string }) | null;
  /** Headline. Words wrapped in *asterisks* get the gradient accent. */
  headline?: string;
  description?: string;
  primaryCta?: HeroAuroraLink;
  secondaryCta?: HeroAuroraLink;
  /** Show the floating product mockup below the CTAs. */
  showMockup?: boolean;
  className?: string;
}

const EASE = [0.22, 1, 0.36, 1] as const;

// Aurora ribbons: deterministic paths so server and client render the same.
const BLOBS = [
  { className: "left-[-10%] top-[-20%] h-[520px] w-[620px] bg-violet-500/50 dark:bg-violet-600/40", x: [0, 120, -40, 0], y: [0, 60, 20, 0], d: 18 },
  { className: "right-[-15%] top-[-10%] h-[480px] w-[560px] bg-cyan-400/45 dark:bg-cyan-500/30", x: [0, -100, 40, 0], y: [0, 40, 90, 0], d: 22 },
  { className: "left-[25%] top-[10%] h-[420px] w-[520px] bg-fuchsia-400/40 dark:bg-fuchsia-500/30", x: [0, 80, -90, 0], y: [0, -40, 50, 0], d: 26 },
  { className: "left-[5%] top-[30%] h-[360px] w-[420px] bg-sky-300/40 dark:bg-indigo-500/30", x: [0, -60, 70, 0], y: [0, 50, -30, 0], d: 20 },
] as const;

function parseHeadline(text: string) {
  return text.split(/\s+/).filter(Boolean).map((raw) => {
    const accent = raw.startsWith("*") || raw.endsWith("*");
    return { word: raw.replace(/\*/g, ""), accent };
  });
}

export function HeroAurora({
  announcement = { tag: "New", label: "Lumen 3.0 is live — meet the realtime canvas", href: "#" },
  headline = "The analytics platform your *whole team* will actually use",
  description = "Lumen turns raw events into answers in seconds. Build live dashboards, share insights, and ship decisions without waiting on a data team.",
  primaryCta = { label: "Start free trial", href: "#" },
  secondaryCta = { label: "Watch the demo", href: "#" },
  showMockup = true,
  className,
}: HeroAuroraProps) {
  const reduce = useReducedMotion();
  const words = React.useMemo(() => parseHeadline(headline), [headline]);

  const rise = (delay: number): Pick<MotionProps, "initial" | "animate" | "transition"> => ({
    initial: reduce ? { opacity: 0 } : { opacity: 0, y: 18, filter: "blur(6px)" },
    animate: { opacity: 1, y: 0, filter: "blur(0px)" },
    transition: { duration: 0.7, delay, ease: EASE },
  });

  const wordsDone = 0.25 + words.length * 0.06;

  return (
    <section className={cn("relative isolate w-full overflow-hidden bg-background text-foreground", className)}>
      {/* Aurora background */}
      <div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
        {BLOBS.map((b, i) => (
          <motion.div
            key={i}
            className={cn("absolute rounded-full blur-[90px]", b.className)}
            animate={reduce ? undefined : { x: [...b.x], y: [...b.y], scale: [1, 1.12, 0.95, 1] }}
            transition={{ duration: b.d, repeat: Infinity, ease: "easeInOut" }}
          />
        ))}
        {/* grid texture + fade into the page */}
        <div className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-foreground)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-foreground)_1px,transparent_1px)] bg-[size:56px_56px] opacity-[0.04] [mask-image:radial-gradient(ellipse_70%_60%_at_50%_30%,#000_40%,transparent_100%)]" />
        <div className="absolute inset-x-0 bottom-0 h-2/3 bg-linear-to-b from-transparent via-background/70 to-background" />
      </div>

      <div className="mx-auto flex max-w-6xl flex-col items-center px-5 pb-16 pt-16 text-center sm:px-8 sm:pt-24">
        {announcement && (
          <motion.a
            href={announcement.href ?? "#"}
            {...rise(0)}
            className="group mb-8 inline-flex max-w-full items-center gap-2 rounded-full border border-foreground/10 bg-background/60 py-1 pl-1 pr-3 text-xs font-medium shadow-sm backdrop-blur-md transition hover:border-foreground/20 hover:bg-background/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:text-sm"
          >
            {announcement.tag && (
              <span className="inline-flex items-center gap-1 rounded-full bg-foreground px-2 py-0.5 text-[11px] font-semibold text-background">
                <Sparkles className="size-3" aria-hidden />
                {announcement.tag}
              </span>
            )}
            <span className="truncate text-foreground/80">{announcement.label}</span>
            <ArrowRight className="size-3.5 shrink-0 text-foreground/60 transition-transform group-hover:translate-x-0.5" aria-hidden />
          </motion.a>
        )}

        <h1 className="max-w-4xl text-balance text-[2.6rem] font-semibold leading-[1.05] tracking-tight sm:text-6xl lg:text-7xl">
          <span className="sr-only">{words.map((w) => w.word).join(" ")}</span>
          <span aria-hidden>
            {words.map((w, i) => (
              <React.Fragment key={i}>
                <motion.span
                  className={cn(
                    "inline-block",
                    w.accent && "bg-linear-to-r from-violet-600 via-fuchsia-500 to-cyan-500 bg-clip-text pb-1 text-transparent dark:from-violet-400 dark:via-fuchsia-400 dark:to-cyan-300",
                  )}
                  initial={reduce ? { opacity: 0 } : { opacity: 0, y: "0.45em", filter: "blur(8px)" }}
                  animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
                  transition={{ duration: 0.65, delay: 0.15 + i * 0.06, ease: EASE }}
                >
                  {w.word}
                </motion.span>
                {i < words.length - 1 && " "}
              </React.Fragment>
            ))}
          </span>
        </h1>

        <motion.p {...rise(wordsDone)} className="mt-6 max-w-2xl text-pretty text-base leading-relaxed text-muted-foreground sm:text-lg">
          {description}
        </motion.p>

        <motion.div {...rise(wordsDone + 0.1)} className="mt-9 flex w-full flex-col items-center justify-center gap-3 sm:w-auto sm:flex-row">
          <a
            href={primaryCta.href ?? "#"}
            className="group inline-flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-6 text-sm font-semibold text-background shadow-lg shadow-foreground/10 transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background sm:w-auto"
          >
            {primaryCta.label}
            <ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" aria-hidden />
          </a>
          <a
            href={secondaryCta.href ?? "#"}
            className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-full border border-foreground/10 bg-background/60 px-6 text-sm font-semibold backdrop-blur-md transition hover:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:w-auto"
          >
            <span className="grid size-5 place-items-center rounded-full bg-foreground/10">
              <Play className="size-2.5 fill-current" aria-hidden />
            </span>
            {secondaryCta.label}
          </a>
        </motion.div>

        {showMockup && (
          <motion.div
            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 60, scale: 0.96 }}
            animate={{ opacity: 1, y: 0, scale: 1 }}
            transition={{ duration: 1, delay: wordsDone + 0.25, ease: EASE }}
            className="relative mt-14 w-full max-w-4xl"
          >
            <motion.div
              animate={reduce ? undefined : { y: [0, -10, 0] }}
              transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }}
            >
              <Mockup />
            </motion.div>
          </motion.div>
        )}
      </div>
    </section>
  );
}

const BARS = [38, 52, 44, 63, 58, 71, 66, 82, 76, 90, 84, 96];

function Mockup() {
  return (
    <div aria-hidden className="relative">
      <div className="absolute -inset-4 -z-10 rounded-[2rem] bg-linear-to-b from-violet-500/20 to-cyan-400/10 blur-2xl" />
      <div className="overflow-hidden rounded-2xl border border-foreground/10 bg-card/80 text-left shadow-2xl shadow-violet-950/10 backdrop-blur-xl">
        <div className="flex items-center gap-2 border-b border-foreground/5 px-4 py-3">
          <span className="size-2.5 rounded-full bg-rose-400/80" />
          <span className="size-2.5 rounded-full bg-amber-400/80" />
          <span className="size-2.5 rounded-full bg-emerald-400/80" />
          <div className="mx-auto h-5 w-40 rounded-md bg-muted sm:w-64" />
        </div>
        <div className="grid gap-4 p-4 sm:grid-cols-[1fr_1.6fr] sm:p-6">
          <div className="grid grid-cols-2 gap-3 sm:grid-cols-1">
            {[
              { k: "Active users", v: "48,210", d: "+12.4%" },
              { k: "Conversion", v: "6.82%", d: "+1.9%" },
            ].map((s) => (
              <div key={s.k} className="rounded-xl border border-foreground/5 bg-background/60 p-3 sm:p-4">
                <p className="text-[11px] text-muted-foreground sm:text-xs">{s.k}</p>
                <p className="mt-1 text-lg font-semibold tabular-nums sm:text-2xl">{s.v}</p>
                <p className="mt-1 inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600 dark:text-emerald-400">
                  <TrendingUp className="size-3" /> {s.d}
                </p>
              </div>
            ))}
            <div className="col-span-2 hidden rounded-xl border border-foreground/5 bg-background/60 p-4 sm:col-span-1 sm:block">
              {["Onboarding flow", "Pricing page", "Checkout"].map((t, i) => (
                <div key={t} className="flex items-center gap-2 py-1 text-xs">
                  <span className="grid size-4 place-items-center rounded-full bg-violet-500/15 text-violet-600 dark:text-violet-300">
                    <Check className="size-2.5" />
                  </span>
                  <span className="text-foreground/80">{t}</span>
                  <span className="ml-auto tabular-nums text-muted-foreground">{[92, 78, 64][i]}%</span>
                </div>
              ))}
            </div>
          </div>
          <div className="rounded-xl border border-foreground/5 bg-background/60 p-4">
            <div className="flex items-center justify-between">
              <p className="text-xs font-medium">Weekly events</p>
              <div className="flex gap-1">
                {["7d", "30d", "90d"].map((t, i) => (
                  <span key={t} className={cn("rounded-md px-2 py-0.5 text-[10px]", i === 1 ? "bg-foreground text-background" : "text-muted-foreground")}>
                    {t}
                  </span>
                ))}
              </div>
            </div>
            <div className="mt-4 flex h-32 items-end gap-1.5 sm:h-44 sm:gap-2">
              {BARS.map((h, i) => (
                <motion.div
                  key={i}
                  className="flex-1 rounded-t-md bg-linear-to-t from-violet-500/70 to-fuchsia-400/80 dark:from-violet-500/80 dark:to-cyan-300/80"
                  initial={{ height: 0 }}
                  animate={{ height: `${h}%` }}
                  transition={{ duration: 0.9, delay: 1.3 + i * 0.04, ease: EASE }}
                />
              ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

More in Heroes

View all →