A portfolio hero: project cards on the arc, with a pinned headline and a caption for the project at the top.
@keyframeui
registry/demos/arc-scroll-case-studies-demo.tsx
"use client";
import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { ArcScroll } from "@/components/ui/arc-scroll";
const projects = [
{ client: "Lumen", kind: "Brand site", year: 2026, domain: "lumen.energy", accent: "from-amber-300 to-orange-500" },
{ client: "Parcel", kind: "Logistics dashboard", year: 2025, domain: "parcel.app", accent: "from-indigo-400 to-blue-600" },
{ client: "Halcyon", kind: "Patient app", year: 2025, domain: "halcyon.health", accent: "from-teal-300 to-emerald-600" },
{ client: "Fieldnote", kind: "Journaling app", year: 2024, domain: "fieldnote.co", accent: "from-lime-300 to-green-600" },
{ client: "Orbit", kind: "Mobile banking", year: 2023, domain: "orbit.bank", accent: "from-violet-400 to-purple-700" },
{ client: "Kiln", kind: "Ceramics store", year: 2022, domain: "kiln.shop", accent: "from-rose-300 to-red-500" },
{ client: "Atlas", kind: "Maps platform", year: 2021, domain: "atlas.dev", accent: "from-sky-300 to-cyan-600" },
];
// For a full-page section, drop the scroll box and `containerRef`: the stage
// defaults to the viewport height and pins while the page scrolls.
export function ArcScrollCaseStudiesDemo() {
const scrollRef = React.useRef<HTMLDivElement>(null);
const [active, setActive] = React.useState(0);
const reduceMotion = useReducedMotion();
const project = projects[active];
return (
<div className="relative w-full overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950">
<div ref={scrollRef} className="relative h-[var(--demo-height,36rem)] overflow-y-auto overscroll-contain">
<ArcScroll
containerRef={scrollRef}
stageClassName="h-[var(--demo-height,36rem)]"
spacing={22}
scrollPerCard={260}
onActiveChange={setActive}
overlay={
<>
<div className="absolute inset-x-0 top-8 px-6 text-center">
<p className="font-mono text-xs uppercase tracking-widest text-zinc-500">
Selected work
</p>
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl dark:text-zinc-50">
2021–2026
</h2>
</div>
<motion.div
key={project.client}
initial={{ opacity: 0, y: reduceMotion ? 0 : 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, ease: "easeOut" }}
className="absolute inset-x-0 bottom-10 px-6 text-center"
>
<p className="font-mono text-xs text-zinc-500 tabular-nums">
{String(active + 1).padStart(2, "0")} /{" "}
{String(projects.length).padStart(2, "0")} · {project.year}
</p>
<p className="mt-1 text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
{project.client}
</p>
<p className="text-sm text-zinc-500">{project.kind}</p>
</motion.div>
</>
}
>
{projects.map((item) => (
<BrowserCard key={item.client} domain={item.domain} accent={item.accent} />
))}
</ArcScroll>
</div>
</div>
);
}
function BrowserCard({ domain, accent }: { domain: string; accent: string }) {
return (
<div className="w-52 overflow-hidden rounded-xl bg-white shadow-xl ring-1 shadow-zinc-900/15 ring-zinc-900/10 sm:w-64 dark:bg-zinc-900 dark:shadow-black/50 dark:ring-white/10">
<div className="flex items-center gap-1.5 border-b border-zinc-200 px-3 py-2 dark:border-zinc-800">
<span className="size-2 rounded-full bg-zinc-300 dark:bg-zinc-700" />
<span className="size-2 rounded-full bg-zinc-300 dark:bg-zinc-700" />
<span className="size-2 rounded-full bg-zinc-300 dark:bg-zinc-700" />
<span className="ml-2 truncate rounded bg-zinc-100 px-2 py-0.5 font-mono text-[10px] text-zinc-500 dark:bg-zinc-800">
{domain}
</span>
</div>
<div className={`flex aspect-[16/10] flex-col justify-end gap-2 bg-linear-to-br p-4 ${accent}`}>
<span className="h-2.5 w-3/4 rounded-full bg-white/80" />
<span className="h-2.5 w-1/2 rounded-full bg-white/60" />
<span className="mt-2 h-5 w-16 rounded-full bg-white/90" />
</div>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/arc-scroll#arc-scroll-case-studies-demo






















