← FeaturesOpen in v0
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-02Code
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>
)
}