# How It Works Section v2

> Timeline-style three-step process section with a GSAP ScrollTrigger beam that travels between week badges as you scroll, revealing each step.

A shadcn/ui block from the [@uiception](https://uiception.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uiception/how-it-works-section-v2).

- Page, with a live preview: https://shadcn.exchange/r/uiception/how-it-works-section-v2
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uiception/how-it-works-section-v2-light-a6c302744c16f196.png
- Registry item: https://uiception.com/r/how-it-works-section-v2.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uiception.com/r/how-it-works-section-v2.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: `gsap`
- Registry items: `badge`

## Files

### `registry/new-york/blocks/how-it-works-section-v2/page.tsx`

```tsx
import HowItWorksSectionV2 from "./components/how-it-works-section-v2"

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

### `registry/new-york/blocks/how-it-works-section-v2/components/how-it-works-section-v2.tsx`

```tsx
"use client"

import StepsGrid from "./steps-grid"

export default function HowItWorksSectionV2() {
  return (
    <section className="py-4 md:py-6 lg:py-8">
      <StepsGrid
        title={
          <h2 className="text-4xl leading-tight font-light tracking-tight sm:text-5xl lg:text-6xl">
            How It Works
          </h2>
        }
      />
    </section>
  )
}
```

### `registry/new-york/blocks/how-it-works-section-v2/components/steps-grid.tsx`

```tsx
"use client"

import type { ReactNode } from "react"
import { useRef } from "react"

import { Badge } from "@/components/ui/badge"

import { useHiw2V2Animations } from "../hooks/use-hiw2-v2-animations"
import { STEP_LABEL_BADGE_VARIANT, steps } from "../lib/steps"
import StepCard from "./step-card"

type StepsGridProps = {
  title: ReactNode
}

export default function StepsGrid({ title }: StepsGridProps) {
  const titleRef = useRef<HTMLDivElement>(null)
  const cardRefs = useRef<(HTMLDivElement | null)[]>([])

  useHiw2V2Animations({
    title: titleRef,
    cards: cardRefs,
  })

  return (
    <div className="mx-auto max-w-6xl px-4">
      <div ref={titleRef} className="mb-12 md:mb-16">
        {title}
      </div>

      <div className="grid grid-cols-1 gap-10 md:grid-cols-3 md:gap-8 lg:gap-14">
        {steps.map((step, i) => (
          <div
            key={step.id}
            ref={(el) => {
              cardRefs.current[i] = el
            }}
          >
            <Badge variant={STEP_LABEL_BADGE_VARIANT} className="uppercase">
              {step.label}
            </Badge>

            <StepCard step={step} />
          </div>
        ))}
      </div>
    </div>
  )
}
```

### `registry/new-york/blocks/how-it-works-section-v2/components/step-card.tsx`

```tsx
import { CheckCircle2 } from "lucide-react"

import type { Step } from "../lib/steps"

type Props = {
  step: Step
}

export default function StepCard({ step }: Props) {
  return (
    <div className="flex flex-col gap-4 pt-4">
      <h3 className="text-xl leading-snug font-medium tracking-tight text-foreground">
        {step.title}
      </h3>

      <ul className="flex flex-col gap-3">
        {step.features.map((feature, index) => (
          <li
            key={index}
            data-hiw2-feature=""
            className="flex items-start gap-3 text-sm leading-relaxed text-muted-foreground"
          >
            <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 fill-foreground stroke-background text-foreground" />
            <span>{feature}</span>
          </li>
        ))}
      </ul>
    </div>
  )
}
```

### `registry/new-york/blocks/how-it-works-section-v2/hooks/use-hiw2-v2-animations.ts`

```ts
"use client"

import { type RefObject, useLayoutEffect, useRef } from "react"

import gsap from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"

gsap.registerPlugin(ScrollTrigger)

export type Hiw2V2Refs = {
  title: RefObject<HTMLElement | null>
  cards: RefObject<(HTMLDivElement | null)[]>
}

export function useHiw2V2Animations(refs: Hiw2V2Refs) {
  const refsRef = useRef(refs)
  useLayoutEffect(() => {
    refsRef.current = refs
  })

  useLayoutEffect(() => {
    let ctx: gsap.Context | null = null
    const r = () => refsRef.current

    const cardEls = (r().cards.current ?? []).filter(Boolean) as HTMLElement[]
    if (!cardEls.length) return

    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      const titleEl = r().title.current
      const allFeatures = cardEls.flatMap((card) =>
        Array.from(card.querySelectorAll<HTMLElement>("[data-hiw2-feature]"))
      )
      gsap.set([titleEl, ...cardEls, ...allFeatures].filter(Boolean), {
        clearProps: "all",
      })
      return
    }

    ctx = gsap.context(() => {
      const titleEl = r().title.current

      if (titleEl) {
        gsap.set(titleEl, { y: 28, opacity: 0 })
        gsap.to(titleEl, {
          y: 0,
          opacity: 1,
          duration: 0.8,
          ease: "power3.out",
          scrollTrigger: {
            trigger: titleEl,
            start: "top 88%",
            once: true,
                      },
        })
      }

      cardEls.forEach((card) => {
        gsap.set(card, { y: 20, opacity: 0 })
        gsap.to(card, {
          y: 0,
          opacity: 1,
          duration: 0.6,
          ease: "power3.out",
          scrollTrigger: {
            trigger: card,
            start: "top 84%",
            once: true,
          },
        })
      })

      cardEls.forEach((card, i) => {
        const features = Array.from(
          card.querySelectorAll<HTMLElement>("[data-hiw2-feature]")
        )
        if (!features.length) return

        gsap.set(features, { x: -12, opacity: 0 })
        gsap.to(features, {
          x: 0,
          opacity: 1,
          stagger: 0.075,
          duration: 0.44,
          ease: "power2.out",
          delay: 0.2 + i * 0.08,
          scrollTrigger: {
            trigger: card,
            start: "top 84%",
            once: true,
                      },
        })
      })

      requestAnimationFrame(() => {
        ScrollTrigger.refresh()
      })
    })

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

### `registry/new-york/blocks/how-it-works-section-v2/lib/steps.ts`

```ts
export const STEP_LABEL_BADGE_VARIANT = "outline" as const

export type StepFeature = string

export type Step = {
  id: string
  label: string
  title: string
  features: StepFeature[]
}

export const steps: Step[] = [
  {
    id: "step-1",
    label: "Step 1",
    title: "Create your project",
    features: [
      "Scaffold a new Next.js app with a single command",
      "Choose Tailwind CSS and App Router when prompted",
      "TypeScript support included out of the box",
    ],
  },
  {
    id: "step-2",
    label: "Step 2",
    title: "Initialize shadcn/ui",
    features: [
      "Run the shadcn CLI to configure your project",
      "Sets up components.json with your preferred style",
      "Adds the necessary Tailwind and import alias config",
    ],
  },
  {
    id: "step-3",
    label: "Step 3",
    title: "Add components",
    features: [
      "Install only the components you actually need",
      "Code is copied directly into your project",
      "Fully customizable — no black-box dependencies",
    ],
  },
]
```
