Features

Features 02 — Alternating Rows

Three alternating feature rows pairing icon, copy and check-marked bullet lists with CSS-only mock visuals, each revealed on scroll.

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

Code

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

"use client"

import * as React from "react"
import { motion } from "motion/react"
import {
  BarChart3,
  Check,
  Plug,
  Workflow,
  type LucideIcon,
} from "lucide-react"

import { fadeUp, scaleIn, stagger, viewport } from "@/lib/motion"
import { cn } from "@/lib/utils"

export type FeatureRow = {
  icon: LucideIcon
  title: string
  description: string
  bullets: string[]
  /** Replace the CSS-only mock for this row. */
  visual?: React.ReactNode
}

export type Features02Props = {
  eyebrow?: string
  title?: React.ReactNode
  description?: string
  features?: FeatureRow[]
  /** Heading element — use "h1" when this is the page's main heading. */
  as?: "h1" | "h2"
  className?: string
}

function ChartMock() {
  return (
    <div className="relative overflow-hidden rounded-2xl border bg-card p-5 shadow-sm">
      <div className="flex items-center justify-between">
        <div className="h-4 w-28 rounded bg-muted" />
        <div className="h-5 w-20 rounded-full bg-primary/15" />
      </div>
      <div className="mt-4 grid grid-cols-2 gap-3">
        {[0, 1].map((i) => (
          <div key={i} className="rounded-lg border bg-background p-3">
            <div className="h-2.5 w-1/2 rounded bg-muted" />
            <div
              className={cn(
                "mt-2.5 h-4 rounded",
                i === 0 ? "w-2/3 bg-primary/40" : "w-1/2 bg-muted"
              )}
            />
          </div>
        ))}
      </div>
      <div className="mt-4 flex h-28 items-end gap-2 rounded-lg border bg-background p-3">
        {[40, 60, 45, 75, 55, 90, 70, 100, 65, 85].map((h, i) => (
          <div
            key={i}
            style={{ height: `${h}%` }}
            className={cn(
              "flex-1 rounded-t-sm",
              i === 7 ? "bg-primary" : "bg-primary/40"
            )}
          />
        ))}
      </div>
    </div>
  )
}

function FlowMock() {
  const rows = [
    { w: "w-2/5", active: true },
    { w: "w-3/5", active: false },
    { w: "w-1/2", active: false },
    { w: "w-2/3", active: false },
  ]
  return (
    <div className="relative overflow-hidden rounded-2xl border bg-card p-5 shadow-sm">
      <div className="flex items-center justify-between">
        <div className="h-4 w-32 rounded bg-muted" />
        <div className="size-6 rounded-md bg-primary/15" />
      </div>
      <div className="mt-4 flex flex-col gap-2.5">
        {rows.map((row, i) => (
          <div
            key={i}
            className={cn(
              "flex items-center gap-3 rounded-lg border bg-background p-3",
              row.active && "border-primary/40"
            )}
          >
            <span
              className={cn(
                "size-2 shrink-0 rounded-full",
                row.active
                  ? "animate-pulse bg-primary"
                  : "bg-muted-foreground/30"
              )}
            />
            <div className={cn("h-3 rounded bg-muted", row.w)} />
            <div
              className={cn(
                "ml-auto h-5 w-14 shrink-0 rounded-full",
                row.active ? "bg-primary/20" : "bg-muted"
              )}
            />
          </div>
        ))}
      </div>
    </div>
  )
}

function CodeMock() {
  const lines: { indent: string; segments: [string, string][] }[] = [
    { indent: "", segments: [["w-12", "bg-primary/40"], ["w-24", "bg-muted-foreground/30"]] },
    { indent: "pl-5", segments: [["w-20", "bg-muted"], ["w-14", "bg-primary/30"]] },
    { indent: "pl-5", segments: [["w-28", "bg-muted-foreground/30"], ["w-10", "bg-muted"]] },
    { indent: "pl-10", segments: [["w-16", "bg-primary/40"], ["w-20", "bg-muted"]] },
    { indent: "pl-10", segments: [["w-24", "bg-muted"]] },
    { indent: "pl-5", segments: [["w-12", "bg-muted-foreground/30"]] },
    { indent: "", segments: [["w-8", "bg-primary/40"]] },
  ]
  return (
    <div className="relative overflow-hidden rounded-2xl border bg-card shadow-sm">
      <div className="flex items-center gap-1.5 border-b px-4 py-2.5">
        <span className="size-2 rounded-full bg-muted-foreground/25" />
        <span className="size-2 rounded-full bg-muted-foreground/25" />
        <span className="size-2 rounded-full bg-muted-foreground/25" />
        <div className="ml-3 h-4 w-24 rounded bg-muted" />
      </div>
      <div className="flex flex-col gap-3 p-5">
        {lines.map((line, i) => (
          <div key={i} className={cn("flex items-center gap-2", line.indent)}>
            {line.segments.map(([w, tone], j) => (
              <div key={j} className={cn("h-2.5 rounded", w, tone)} />
            ))}
          </div>
        ))}
      </div>
    </div>
  )
}

const defaultVisuals = [ChartMock, FlowMock, CodeMock]

const defaultFeatures: FeatureRow[] = [
  {
    icon: BarChart3,
    title: "Understand every user journey",
    description:
      "Follow people from first click to power user. Lumen stitches sessions across devices so you see the whole story, not fragments.",
    bullets: [
      "Funnels and retention charts that build themselves",
      "Session timelines with every event in context",
      "Cohorts you can save, share and act on",
    ],
  },
  {
    icon: Workflow,
    title: "Automate the busywork",
    description:
      "Turn insights into action without leaving the dashboard. Trigger flows when metrics move, and let Lumen watch the numbers overnight.",
    bullets: [
      "Alerts when any metric drifts from its baseline",
      "Scheduled digests delivered where your team works",
      "No-code triggers for your existing workflow tools",
    ],
  },
  {
    icon: Plug,
    title: "Plays well with your stack",
    description:
      "One SDK, every platform. Stream events in, sync insights out — your data warehouse stays the source of truth.",
    bullets: [
      "Typed SDKs for web, mobile and server",
      "Two-way warehouse sync with zero pipelines",
      "A GraphQL API for anything we didn't think of",
    ],
  },
]

export function Features02({
  eyebrow = "How it works",
  title = "Built for the way modern teams ship",
  description = "Three ideas, one platform: see what users do, act on it automatically, and keep your stack in sync.",
  features = defaultFeatures,
  as: Heading = "h2",
  className,
}: Features02Props) {
  return (
    <section className={cn("relative bg-background px-6 py-24 md:py-32", className)}>
      <div className="mx-auto max-w-6xl">
        <motion.div
          initial="hidden"
          whileInView="visible"
          viewport={viewport}
          variants={stagger()}
          className="mx-auto max-w-2xl text-center"
        >
          <motion.p
            variants={fadeUp}
            className="text-sm font-medium tracking-widest text-primary uppercase"
          >
            {eyebrow}
          </motion.p>
          <motion.div variants={fadeUp}>
            <Heading className="mt-4 text-balance font-heading text-4xl font-semibold tracking-tight md:text-5xl">
              {title}
            </Heading>
          </motion.div>
          <motion.p
            variants={fadeUp}
            className="mt-5 text-lg text-pretty text-muted-foreground"
          >
            {description}
          </motion.p>
        </motion.div>

        <div className="mt-20 flex flex-col gap-20 md:gap-28">
          {features.map((feature, index) => {
            const DefaultVisual = defaultVisuals[index % defaultVisuals.length]
            return (
              <motion.div
                key={feature.title}
                initial="hidden"
                whileInView="visible"
                viewport={viewport}
                variants={stagger(0.12)}
                className="grid items-center gap-10 md:grid-cols-2 md:gap-16"
              >
                <motion.div
                  variants={fadeUp}
                  className={cn(index % 2 === 1 && "md:order-last")}
                >
                  <div className="flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary ring-1 ring-primary/15">
                    <feature.icon className="size-5" />
                  </div>
                  <h3 className="mt-6 font-heading text-2xl font-semibold tracking-tight md:text-3xl">
                    {feature.title}
                  </h3>
                  <p className="mt-4 text-pretty text-muted-foreground">
                    {feature.description}
                  </p>
                  <ul className="mt-6 space-y-3">
                    {feature.bullets.map((bullet) => (
                      <li
                        key={bullet}
                        className="flex items-start gap-3 text-sm text-muted-foreground"
                      >
                        <Check className="mt-0.5 size-4 shrink-0 text-primary" />
                        {bullet}
                      </li>
                    ))}
                  </ul>
                </motion.div>

                <motion.div variants={scaleIn} className="relative">
                  <div
                    aria-hidden
                    className="absolute -inset-4 rounded-[2rem] bg-primary/10 blur-2xl"
                  />
                  <div className="relative">
                    {feature.visual ?? <DefaultVisual />}
                  </div>
                </motion.div>
              </motion.div>
            )
          })}
        </div>
      </div>
    </section>
  )
}