# Hero Section v4

> Centered hero with headline, description, dual CTA buttons, and a stats section with five-star rating below.

A shadcn/ui block from the [@uiception](https://uiception.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uiception/hero-section-v4).

- Page, with a live preview: https://shadcn.exchange/r/uiception/hero-section-v4
- Screenshot: https://shadcn.exchange/shot/uiception/hero-section-v4/light.png
- Registry item: https://uiception.com/r/hero-section-v4.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/uiception/hero-section-v4.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@paper-design/shaders-react`, `gsap`, `motion`
- Registry items: `https://uiception.com/r/block-media.json`, `button`, `https://uiception.com/r/stats-section-v1.json`

## Files

### `registry/new-york/blocks/hero-section-v4/page.tsx`

```tsx
import HeroSectionV4 from "./components/hero-section-v4"

export default function Page() {
  return <HeroSectionV4 />
}
```

### `registry/new-york/blocks/hero-section-v4/components/hero-section-v4.tsx`

```tsx
import { StatsSectionV1Content } from "../../stats-section-v1/components/stats-section-v1"
import { createBlockImage } from "@/lib/block-media"

const blockImage = createBlockImage("hero-section-v4")
import { HeroV4Root } from "./hero-v4-root"

import "../styles/hero-section-v4.css"


export default function HeroSectionV4() {
  return (
    <section className="py-4 md:py-6 lg:py-8">
      <HeroV4Root bgSrc={blockImage("image.png")}>
        <StatsSectionV1Content />
      </HeroV4Root>
    </section>
  )
}
```

### `registry/new-york/blocks/hero-section-v4/components/hero-v4-root.tsx`

```tsx
"use client"

import type { ReactNode } from "react"
import { useLayoutEffect, useRef } from "react"
import gsap from "gsap"
import { MotionConfig, motion } from "motion/react"

import HeroContent from "./hero-content"
import { HeroHalftoneImage } from "./hero-halftone-image"

const MEDIA_DELAY_S = 1.1

type HeroV4RootProps = {
  children?: ReactNode
  bgSrc: string
}

export function HeroV4Root({ children, bgSrc }: HeroV4RootProps) {
  const copyRootRef = useRef<HTMLDivElement>(null)

  useLayoutEffect(() => {
    const root = copyRootRef.current
    if (!root) return

    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      gsap.set(
        root.querySelectorAll("[data-hero-v4-reveal], [data-hero-v4-word]"),
        {
          clearProps: "all",
        }
      )
      return
    }

    const ctx = gsap.context(() => {
      const reveals = gsap.utils.toArray<HTMLElement>("[data-hero-v4-reveal]")
      const words = gsap.utils.toArray<HTMLElement>("[data-hero-v4-word]")

      const tl = gsap.timeline()

      tl.fromTo(
        words,
        { opacity: 0, filter: "blur(9px)", x: 20 },
        {
          opacity: 1,
          filter: "blur(0px)",
          x: 0,
          duration: 0.54,
          stagger: { each: 0.08, from: "start" },
          ease: "power3.out",
          clearProps: "transform,filter",
        }
      )

      tl.fromTo(
        reveals[0],
        { opacity: 0, y: 16 },
        {
          opacity: 1,
          y: 0,
          duration: 0.55,
          ease: "power2.out",
          clearProps: "transform",
        },
        "-=0.22"
      )

      tl.fromTo(
        reveals[1],
        { opacity: 0, y: 10, scale: 0.96 },
        {
          opacity: 1,
          y: 0,
          scale: 1,
          duration: 0.48,
          ease: "power2.out",
          clearProps: "transform",
        },
        "-=0.28"
      )
    }, root)

    return () => ctx.revert()
  }, [])

  return (
    <MotionConfig reducedMotion="user">
      <div ref={copyRootRef} className="mx-auto max-w-6xl px-4">
        <div className="flex flex-col items-center gap-6 text-center">
          {children}
          <HeroContent />
        </div>
      </div>

      <div className="mx-auto max-w-4xl px-4 pt-4">
        <motion.div
          className="hero-v4__media relative aspect-video w-full overflow-hidden rounded-xl"
          initial={{ opacity: 0, y: 28, scale: 0.98 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          transition={{
            delay: MEDIA_DELAY_S,
            duration: 0.72,
            ease: [0.22, 1, 0.36, 1],
          }}
        >
          <HeroHalftoneImage src={bgSrc} />
        </motion.div>
      </div>
    </MotionConfig>
  )
}
```

### `registry/new-york/blocks/hero-section-v4/components/hero-content.tsx`

```tsx
import { IBM_Plex_Serif } from "next/font/google"

import CtaButtons from "./cta-buttons"

const ibmPlexSerif = IBM_Plex_Serif({
  subsets: ["latin"],
  weight: ["400", "500", "600"],
})

const TITLE_WORDS = ["Turn", "AI", "from", "noise", "into", "results."]

export default function HeroContent() {
  return (
    <div className="flex flex-col items-center gap-6 text-center">
      <h1
        className={`${ibmPlexSerif.className} text-3xl tracking-tighter sm:text-4xl lg:text-5xl lg:leading-[1.15]`}
      >
        {TITLE_WORDS.map((word, i) => (
          <span key={i} data-hero-v4-word className="inline-block">
            {i < TITLE_WORDS.length - 1 ? word + "\u00A0" : word}
          </span>
        ))}
      </h1>

      <div data-hero-v4-reveal>
        <p className="max-w-xl text-base leading-7 tracking-tight text-muted-foreground sm:text-lg sm:leading-8">
          We find where AI can save you time and money, then build the systems
          that make it happen.
        </p>
      </div>

      <div data-hero-v4-reveal>
        <CtaButtons />
      </div>
    </div>
  )
}
```

### `registry/new-york/blocks/hero-section-v4/components/cta-buttons.tsx`

```tsx
"use client"

import Link from "next/link"
import { useState } from "react"
import { CirclePlay } from "lucide-react"

import { Button } from "@/components/ui/button"

const PRIMARY_HREF = "#"
const SECONDARY_HREF = "#"

const LABEL = "How we work"
const CHAR_DELAY_MS = 7
const TOTAL_MS = LABEL.length * CHAR_DELAY_MS

export default function CtaButtons() {
  const [hovered, setHovered] = useState(false)

  return (
    <div className="flex flex-row flex-wrap justify-center gap-3">

      <Button size="lg" className="group relative overflow-hidden" asChild>
        <Link href={PRIMARY_HREF}>

          <span className="absolute inset-0 flex items-center justify-center transition-transform duration-300 ease-in-out group-hover:translate-y-full">
            Book a free call
          </span>

          <span className="absolute inset-0 flex -translate-y-full items-center justify-center transition-transform duration-300 ease-in-out group-hover:translate-y-0">
            Book a free call
          </span>

          <span className="invisible">Book a free call</span>
        </Link>
      </Button>

      <Button
        variant="outline"
        size="lg"
        className="relative overflow-hidden"
        asChild
      >
        <Link
          href={SECONDARY_HREF}
          onMouseEnter={() => setHovered(true)}
          onMouseLeave={() => setHovered(false)}
        >

          <span className="flex items-center gap-2">
            <span className="flex">
              {LABEL.split("").map((char, i) => (
                <span
                  key={i}
                  className="inline-block transition-opacity duration-75"
                  style={{
                    opacity: hovered ? 0 : 1,
                    transitionDelay: hovered
                      ? `${(LABEL.length - 1 - i) * CHAR_DELAY_MS}ms`
                      : "0ms",
                  }}
                >
                  {char === " " ? "\u00A0" : char}
                </span>
              ))}
            </span>
            <CirclePlay
              size={16}
              className="shrink-0 transition-opacity duration-75"
              style={{ opacity: hovered ? 0 : 1 }}
            />
          </span>

          <CirclePlay
            size={18}
            className="absolute inset-0 m-auto transition-all duration-260"
            style={{
              opacity: hovered ? 1 : 0,
              transform: hovered
                ? "translateX(0px) scale(1.4)"
                : "translateX(14px) scale(0.55)",
              transitionDelay: hovered ? `${TOTAL_MS}ms` : "0ms",
              transitionTimingFunction: "cubic-bezier(0.34, 1.56, 0.64, 1)",
            }}
          />
        </Link>
      </Button>
    </div>
  )
}
```

### `registry/new-york/blocks/hero-section-v4/components/hero-halftone-image.tsx`

```tsx
"use client"

import { HalftoneDots } from "@paper-design/shaders-react"

type HeroHalftoneImageProps = {
  src: string
}

export function HeroHalftoneImage({ src }: HeroHalftoneImageProps) {
  return (
    <HalftoneDots
      aria-hidden={true}
      className="absolute inset-0 h-full w-full"
      colorBack="#f2f1e8"
      colorFront="#2b2b2b"
      contrast={0.5}
      fit="cover"
      grainMixer={0.4}
      grainOverlay={0.2}
      grainSize={0.5}
      grid="hex"
      image={src}
      inverted={false}
      originalColors={false}
      radius={1.5}
      size={0.2}
      type="gooey"
    />
  )
}
```

### `registry/new-york/blocks/hero-section-v4/styles/hero-section-v4.css`

```css
@media (prefers-reduced-motion: no-preference) {
  [data-hero-v4-reveal],
  [data-hero-v4-word] {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  [data-hero-v4-reveal],
  [data-hero-v4-word] {
    opacity: 1;
    transform: none;
    filter: none;
  }
}
```

### `public/images/blocks/hero-section-v4/image.png.gitkeep`

```gitkeep
Place your asset here. Same base name, any supported format: jpg, jpeg, png, webp, avif, gif, or svg. Remove the .gitkeep extension from the filename.
```
