# Process Steps
*URL: https://www.grettech.com/components/process-steps*

An auto-playing, step-by-step process section with an image panel that follows the active step.

- Auto-advances with a progress bar
- Pauses on hover and keyboard focus
- Keyboard-accessible steps
- Respects reduced-motion settings

## Install with the shadcn CLI
```bash
npx shadcn@latest add https://www.grettech.com/api/registry/process-steps
```
With its demo: `npx shadcn@latest add https://www.grettech.com/api/registry/process-steps-demo`

**Dependencies:** npm: clsx, tailwind-merge, motion, lucide-react

## Manual installation
**Step 1: Install dependencies**
Install the packages this component uses and the shadcn/ui components it builds on. It also needs the `cn` helper in `lib/utils.ts`, which `npx shadcn@latest init` adds.

```bash
npm install clsx tailwind-merge motion lucide-react
```

**Step 2: Copy the source code**
Add these files to your project components directory.

File: `components/ui/process-steps.tsx`
```tsx
"use client"

import { cn } from "@/lib/utils"
import {
  AnimatePresence,
  motion,
  useInView,
  useReducedMotion,
} from "motion/react"
import * as React from "react"
import { useEffect, useRef, useState } from "react"

/** How long each step stays active while auto-playing, in ms */
const STEP_DURATION = 5000

export interface ProcessStep {
  title: string
  content: string
  icon: React.ReactNode
  image: string
  step: string
}

export interface ProcessStepsProps extends React.HTMLAttributes<HTMLDivElement> {
  title?: string
  subtitle?: string
  steps: ProcessStep[]
  pauseOnHover?: boolean
}

// --- SUB-COMPONENTS ---

const ProcessStepsHeader = ({
  title,
  subtitle,
}: {
  title: string
  subtitle: string
}) => (
  <div className="relative mx-auto mb-12 max-w-2xl text-center">
    <div className="relative z-10">
      <h2 className="mb-4 bg-linear-to-b from-foreground to-foreground/70 bg-clip-text text-4xl font-bold tracking-tight text-balance text-transparent md:text-5xl lg:text-6xl">
        {title}
      </h2>
      <p className="mx-auto max-w-2xl text-base text-pretty text-muted-foreground md:text-lg">
        {subtitle}
      </p>
    </div>
    <div
      aria-hidden="true"
      className="pointer-events-none absolute inset-0 mx-auto h-44 max-w-xs blur-[118px]"
      style={{
        background:
          "linear-gradient(152.92deg, rgba(59, 130, 246, 0.2) 4.54%, rgba(37, 99, 235, 0.26) 34.2%, rgba(59, 130, 246, 0.1) 77.55%)",
      }}
    />
  </div>
)

const ProcessStepsList = ({
  steps,
  currentFeature,
  onSelect,
  progressRef,
  autoplay,
}: {
  steps: ProcessStep[]
  currentFeature: number
  onSelect: (index: number) => void
  progressRef: React.RefObject<HTMLSpanElement | null>
  autoplay: boolean
}) => (
  <ol className="order-2 space-y-2 md:order-1">
    {steps.map((feature, index) => {
      const active = index === currentFeature
      return (
        <li key={index}>
          <button
            type="button"
            onClick={() => onSelect(index)}
            aria-current={active ? "step" : undefined}
            className={cn(
              "relative flex w-full items-start gap-5 overflow-hidden rounded-2xl p-4 text-left transition-colors duration-300 outline-none focus-visible:ring-2 focus-visible:ring-ring md:gap-6 md:p-5",
              active ? "bg-muted/60" : "hover:bg-muted/30"
            )}
          >
            <span
              className={cn(
                "flex size-12 shrink-0 items-center justify-center rounded-full border-2 transition-colors duration-300 md:size-14 [&_svg]:size-5 md:[&_svg]:size-6",
                active
                  ? "border-primary bg-primary/10 text-primary"
                  : "border-border bg-muted text-muted-foreground"
              )}
            >
              {feature.icon}
            </span>

            <span className="flex-1">
              <span
                className={cn(
                  "block text-lg font-semibold tracking-tight transition-colors duration-300 md:text-xl",
                  active ? "text-foreground" : "text-foreground/70"
                )}
              >
                {feature.title}
              </span>
              <span className="mt-1 block text-sm text-pretty text-muted-foreground md:text-base">
                {feature.content}
              </span>
            </span>

            {/* Time left on the active step */}
            {active && autoplay && (
              <span
                aria-hidden="true"
                className="absolute inset-x-4 bottom-0 h-0.5 overflow-hidden rounded-full bg-border/60 md:inset-x-5"
              >
                <span
                  ref={progressRef}
                  className="block h-full origin-left scale-x-0 bg-primary"
                />
              </span>
            )}
          </button>
        </li>
      )
    })}
  </ol>
)

const ProcessStepsImageDisplay = ({
  steps,
  currentFeature,
}: {
  steps: ProcessStep[]
  currentFeature: number
}) => (
  <div className="relative order-1 h-75 overflow-hidden rounded-2xl border border-border/60 bg-muted shadow-[0_8px_30px_-12px_rgba(0,0,0,0.15)] md:order-2 md:h-100 lg:h-125">
    <AnimatePresence mode="wait" initial={false}>
      {steps.map(
        (feature, index) =>
          index === currentFeature && (
            <motion.div
              key={index}
              className="absolute inset-0"
              initial={{ y: 40, opacity: 0 }}
              animate={{ y: 0, opacity: 1 }}
              exit={{ y: -40, opacity: 0 }}
              transition={{ duration: 0.45, ease: "easeInOut" }}
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={feature.image}
                alt={feature.title}
                loading="lazy"
                decoding="async"
                className="h-full w-full object-cover"
              />
              <div className="absolute inset-x-0 bottom-0 h-2/3 bg-linear-to-t from-background via-background/40 to-transparent" />

              <div className="absolute bottom-4 left-4 flex items-baseline gap-2 rounded-xl border border-border/50 bg-background/90 px-4 py-2 shadow-lg backdrop-blur-md">
                <span className="font-mono text-sm font-semibold text-primary">
                  {feature.step}
                </span>
                <span className="text-sm font-medium text-foreground">
                  {feature.title}
                </span>
              </div>
            </motion.div>
          )
      )}
    </AnimatePresence>
  </div>
)

// --- MAIN COMPONENT ---

export const ProcessSteps = React.forwardRef<HTMLElement, ProcessStepsProps>(
  (
    {
      title = "Our 4-Step Process",
      subtitle = "From strategy to launch, we handle design, development, and growth—end to end.",
      steps,
      pauseOnHover = true,
      className,
      ...props
    },
    ref
  ) => {
    const [currentFeature, setCurrentFeature] = useState(0)
    const [isPaused, setIsPaused] = useState(false)
    const sectionRef = useRef<HTMLElement>(null)
    const progressRef = useRef<HTMLSpanElement>(null)
    const animationRef = useRef<Animation | null>(null)
    const isInView = useInView(sectionRef, { amount: 0.1 })
    const reduceMotion = useReducedMotion()
    const autoplay = !reduceMotion && steps.length > 1

    // The progress bar drives autoplay: when it fills, move to the next step
    useEffect(() => {
      const bar = progressRef.current
      if (!autoplay || !bar) return

      const animation = bar.animate(
        [{ transform: "scaleX(0)" }, { transform: "scaleX(1)" }],
        { duration: STEP_DURATION, easing: "linear", fill: "forwards" }
      )
      animation.pause()
      animation.onfinish = () =>
        setCurrentFeature((prev) => (prev + 1) % steps.length)
      animationRef.current = animation

      return () => {
        animation.onfinish = null
        animation.cancel()
        animationRef.current = null
      }
    }, [autoplay, currentFeature, steps.length])

    // Only run while the section is on screen and not hovered
    useEffect(() => {
      const animation = animationRef.current
      if (!animation) return
      if (isInView && !isPaused) animation.play()
      else animation.pause()
    }, [isInView, isPaused, currentFeature, autoplay])

    return (
      <section
        ref={(node) => {
          sectionRef.current = node
          if (typeof ref === "function") ref(node)
          else if (ref) ref.current = node
        }}
        className={cn("bg-background px-6 py-24 md:px-12 md:py-32", className)}
        onMouseEnter={() => pauseOnHover && setIsPaused(true)}
        onMouseLeave={() => pauseOnHover && setIsPaused(false)}
        onFocus={() => pauseOnHover && setIsPaused(true)}
        onBlur={(e) => {
          if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
            setIsPaused(false)
          }
        }}
        {...props}
      >
        <div className="mx-auto w-full max-w-7xl">
          <ProcessStepsHeader title={title} subtitle={subtitle} />

          <div className="flex flex-col gap-6 md:grid md:grid-cols-2 md:items-center md:gap-10">
            <ProcessStepsList
              steps={steps}
              currentFeature={currentFeature}
              onSelect={setCurrentFeature}
              progressRef={progressRef}
              autoplay={autoplay}
            />
            <ProcessStepsImageDisplay
              steps={steps}
              currentFeature={currentFeature}
            />
          </div>
        </div>
      </section>
    )
  }
)
ProcessSteps.displayName = "ProcessSteps"

```

**Step 3: Framework Adjustments (Optional)**
If you are manually copying this code into a Vite or Remix project (without using the CLI), make sure to remove the `"use client";` directive from the very top of the file.

## Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
| `title` | `string` | `"Our 4-Step Process"` | No | Main heading. |
| `subtitle` | `string` | `"From strategy to launch, we handle design, development, and growth—end to end."` | No | Text under the main heading. |
| `steps` | `ProcessStep[]` | - | Yes | The steps, in order. Any number works; auto-play cycles through them every 5 seconds. |
| `steps.step` | `string` | - | Yes | Short label shown on the image, e.g. "01". |
| `steps.title` | `string` | - | Yes | Step title. |
| `steps.content` | `string` | - | Yes | Text under the step title. |
| `steps.icon` | `React.ReactNode` | - | Yes | Icon in the step's circle. |
| `steps.image` | `string` | - | Yes | URL of the image shown while the step is active. |
| `pauseOnHover` | `boolean` | `true` | No | Pause auto-play while the section is hovered or has keyboard focus. Auto-play is always off for users who prefer reduced motion. |
| `className` | `string` | - | No | Extra classes for the root <section>. |
| `...props` | `React.HTMLAttributes<HTMLElement>` | - | No | Any other section attributes (id, style, etc.). |

## Usage
```tsx
"use client"

import { Code, PenTool, Rocket, Search } from "lucide-react"
import { ProcessSteps } from "@/components/ui/process-steps"

export default function ProcessStepsDemo() {
  const steps = [
    {
      step: "01",
      title: "Discovery & Strategy",
      content:
        "We dive deep into your business goals, target audience, and market landscape to define a clear roadmap for success.",
      icon: <Search className="h-6 w-6" />,
      image:
        "https://images.unsplash.com/photo-1552664730-d307ca884978?q=75&w=1200&auto=format&fit=crop",
    },
    {
      step: "02",
      title: "Design & Prototyping",
      content:
        "Our designers craft intuitive wireframes and stunning visual prototypes to ensure the user experience is flawless.",
      icon: <PenTool className="h-6 w-6" />,
      image:
        "https://images.unsplash.com/photo-1561070791-2526d30994b5?q=75&w=1200&auto=format&fit=crop",
    },
    {
      step: "03",
      title: "Development & Engineering",
      content:
        "Using cutting-edge technologies, our engineers bring the designs to life with robust, scalable, and secure code.",
      icon: <Code className="h-6 w-6" />,
      image:
        "https://images.unsplash.com/photo-1498050108023-c5249f4df085?q=75&w=1200&auto=format&fit=crop",
    },
    {
      step: "04",
      title: "Testing & Launch",
      content:
        "Rigorous QA testing ensures everything works perfectly before we deploy your new digital product to the world.",
      icon: <Rocket className="h-6 w-6" />,
      image:
        "https://images.unsplash.com/photo-1460925895917-afdab827c52f?q=75&w=1200&auto=format&fit=crop",
    },
  ]

  return (
    <div className="relative h-[800px] w-full overflow-hidden overflow-y-auto rounded-xl border border-dashed bg-background">
      <ProcessSteps
        title="How We Work"
        subtitle="A proven 4-step methodology designed to eliminate risk and deliver exceptional results on time and within budget."
        steps={steps}
      />
    </div>
  )
}

```
