# Arc Scroll: Case Studies

> A portfolio hero: project cards on the arc, with a pinned headline and a caption for the project at the top.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/arc-scroll-case-studies-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/arc-scroll-case-studies-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/arc-scroll-case-studies-demo-light-2e809cd3476bf025.png
- Registry item: https://keyframeui.com/r/arc-scroll-case-studies-demo.json
- Categories: scroll, cards, motion
- Listed: 2026-10-14, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/arc-scroll-case-studies-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/arc-scroll.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/arc-scroll#arc-scroll-case-studies-demo

## Files

### `registry/demos/arc-scroll-case-studies-demo.tsx`

```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>
  );
}
```
