Hero

Hero 02 — Split

Split hero with left-aligned copy, avatar-stack social proof and a floating, layered dashboard mock with an SVG sparkline.

$ npx shadcn@latest add @ludusvibe/hero-02
Open in v0

Code

Installed to components/sections/hero-02.tsx — it's yours to edit.

"use client"

import * as React from "react"
import { motion, useReducedMotion, type Transition } from "motion/react"
import { ArrowRight, Bell } from "lucide-react"

import { Badge } from "@/components/ui/badge"
import { buttonVariants } from "@/components/ui/button"
import { EASE, fadeUp, scaleIn, stagger, transition, viewport } from "@/lib/motion"
import { cn } from "@/lib/utils"

type Cta = { label: string; href: string }

export type Hero02Props = {
  eyebrow?: string
  title?: React.ReactNode
  description?: string
  primaryCta?: Cta
  secondaryCta?: Cta
  /** Initials rendered as overlapping avatar circles in the social-proof row. */
  avatars?: string[]
  socialProof?: React.ReactNode
  /** Heading element — use "h1" when this is the page's main heading. */
  as?: "h1" | "h2"
  className?: string
}

const avatarTints = [
  "bg-primary/15 text-primary",
  "bg-muted text-muted-foreground",
  "bg-secondary text-secondary-foreground",
  "bg-primary/10 text-primary",
  "bg-muted text-muted-foreground",
]

/** Slow continuous bob for the mock cards — timing derived from the shared presets. */
const floatTransition: Transition = {
  duration: (transition.duration ?? 1) * 6,
  ease: EASE,
  repeat: Infinity,
  repeatType: "mirror",
}

export function Hero02({
  eyebrow = "Lumen 2.0 — now generally available",
  title = (
    <>
      Every metric that matters, on{" "}
      <span className="text-primary">one calm dashboard</span>
    </>
  ),
  description = "Lumen pulls revenue, product and support signals into a single live view — so your team stops tab-hopping and starts deciding. Connect your stack in minutes, no SQL required.",
  primaryCta = { label: "Start for free", href: "#" },
  secondaryCta = { label: "Book a live demo", href: "#" },
  avatars = ["AK", "MJ", "SD", "RT", "EL"],
  socialProof = (
    <>
      Loved by <span className="font-semibold text-foreground">2,000+</span>{" "}
      builders
    </>
  ),
  as: Heading = "h1",
  className,
}: Hero02Props) {
  const reducedMotion = useReducedMotion()

  return (
    <section className={cn("relative overflow-hidden bg-background", className)}>
      <motion.div
        initial="hidden"
        whileInView="visible"
        viewport={viewport}
        variants={stagger()}
        className="relative mx-auto grid max-w-6xl items-center gap-16 px-6 py-24 md:py-32 lg:grid-cols-2 lg:gap-12"
      >
        {/* Left: copy */}
        <div className="flex flex-col items-start">
          <motion.div variants={fadeUp}>
            <Badge
              variant="outline"
              className="gap-2 rounded-full px-3 py-1 text-sm font-normal text-muted-foreground"
            >
              <span className="size-1.5 rounded-full bg-primary" />
              {eyebrow}
            </Badge>
          </motion.div>

          <motion.div variants={fadeUp}>
            <Heading className="mt-6 max-w-xl text-balance font-heading text-4xl font-semibold tracking-tighter sm:text-5xl md:text-6xl">
              {title}
            </Heading>
          </motion.div>

          <motion.p
            variants={fadeUp}
            className="mt-6 max-w-xl text-lg text-pretty text-muted-foreground"
          >
            {description}
          </motion.p>

          <motion.div
            variants={fadeUp}
            className="mt-9 flex flex-col gap-3 sm:flex-row"
          >
            <a
              href={primaryCta.href}
              className={cn(buttonVariants({ size: "lg" }), "group")}
            >
              {primaryCta.label}
              <ArrowRight className="transition-transform group-hover:translate-x-0.5" />
            </a>
            <a
              href={secondaryCta.href}
              className={buttonVariants({ size: "lg", variant: "outline" })}
            >
              {secondaryCta.label}
            </a>
          </motion.div>

          <motion.div variants={fadeUp} className="mt-10 flex items-center gap-4">
            <div aria-hidden className="flex -space-x-2">
              {avatars.map((initials, i) => (
                <span
                  key={`${initials}-${i}`}
                  className={cn(
                    "flex size-9 items-center justify-center rounded-full text-xs font-semibold ring-2 ring-background",
                    avatarTints[i % avatarTints.length]
                  )}
                >
                  {initials}
                </span>
              ))}
            </div>
            <p className="text-sm text-muted-foreground">{socialProof}</p>
          </motion.div>
        </div>

        {/* Right: layered CSS-only app mock */}
        <motion.div
          variants={scaleIn}
          aria-hidden
          className="relative mx-auto w-full max-w-md"
        >
          <div className="absolute top-1/2 left-1/2 h-[340px] w-[340px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/20 blur-[110px] sm:h-[440px] sm:w-[440px]" />

          {/* Offset back card */}
          <div className="absolute inset-0 translate-x-5 translate-y-6 rotate-3 rounded-2xl border bg-card/50" />

          {/* Main dashboard card */}
          <motion.div
            animate={reducedMotion ? undefined : { y: [-5, 5] }}
            transition={floatTransition}
            className="relative -rotate-1 rounded-2xl border bg-card p-5 shadow-2xl"
          >
            <div className="flex items-center justify-between">
              <div className="space-y-2">
                <div className="h-4 w-28 rounded bg-muted-foreground/30" />
                <div className="h-2.5 w-20 rounded bg-muted" />
              </div>
              <div className="h-6 w-16 rounded-full bg-primary/15" />
            </div>

            {/* Sparkline panel */}
            <div className="mt-5 rounded-lg border bg-background p-4">
              <div className="flex items-center justify-between">
                <div className="h-3 w-24 rounded bg-muted" />
                <div className="h-5 w-16 rounded bg-primary/25" />
              </div>
              <div className="mt-3 text-primary">
                <svg
                  viewBox="0 0 240 64"
                  preserveAspectRatio="none"
                  fill="none"
                  className="h-16 w-full"
                >
                  <polygon
                    points="0,52 24,44 48,48 72,34 96,38 120,24 144,30 168,16 192,22 216,10 240,16 240,64 0,64"
                    fill="currentColor"
                    opacity="0.1"
                  />
                  <polyline
                    points="0,52 24,44 48,48 72,34 96,38 120,24 144,30 168,16 192,22 216,10 240,16"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    vectorEffect="non-scaling-stroke"
                  />
                </svg>
              </div>
            </div>

            {/* Skeleton rows */}
            <div className="mt-4 space-y-3">
              {[0, 1, 2].map((i) => (
                <div key={i} className="flex items-center gap-3">
                  <span className="size-8 shrink-0 rounded-full bg-muted" />
                  <div className="flex-1 space-y-1.5">
                    <div className="h-2.5 w-2/3 rounded bg-muted" />
                    <div className="h-2 w-1/3 rounded bg-muted/60" />
                  </div>
                  <div className="h-3 w-10 rounded bg-primary/20" />
                </div>
              ))}
            </div>
          </motion.div>

          {/* Floating notification card */}
          <motion.div
            animate={reducedMotion ? undefined : { y: [4, -4] }}
            transition={{
              ...floatTransition,
              delay: transition.duration ?? 0,
            }}
            className="absolute -top-6 right-0 w-52 rotate-2 rounded-xl border bg-card p-3 shadow-xl sm:-right-4"
          >
            <div className="flex items-center gap-3">
              <span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/15 text-primary">
                <Bell className="size-4" />
              </span>
              <div className="flex-1 space-y-1.5">
                <div className="h-2.5 w-3/4 rounded bg-muted-foreground/30" />
                <div className="h-2 w-1/2 rounded bg-muted" />
              </div>
            </div>
          </motion.div>
        </motion.div>
      </motion.div>
    </section>
  )
}