# Blueprint: Studio Process

> A case study that goes from an annotated blueprint to the finished design as you scroll, with `play="manual"` following `useScroll`. `data-blueprint-order` groups the build into type, imagery and surfaces, and `onBlockChange` lights up the matching stage in the rail. Scroll back up and it comes apart.

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

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/blueprint-process-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/blueprint-process-demo-light-25ea3f7e417169ef.png
- Registry item: https://keyframeui.com/r/blueprint-process-demo.json
- Categories: layout, scroll, motion
- Listed: 2026-10-14, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/blueprint-process-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/blueprint.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/blueprint#blueprint-process-demo

## Files

### `registry/demos/blueprint-process-demo.tsx`

```tsx
"use client";

import * as React from "react";
import {
  motion,
  useReducedMotion,
  useScroll,
  useSpring,
  useTransform,
  type MotionValue,
} from "motion/react";

import { cn } from "@/lib/utils";
import { Blueprint, type BlueprintState } from "@/components/ui/blueprint";

// Each stage is a `data-blueprint-order` group: type first, then imagery, then surfaces.
const stages = [
  { title: "Wireframe", note: "Blocks measured from the page" },
  { title: "Type", note: "Copy set on its lines" },
  { title: "Imagery", note: "Photos develop in place" },
  { title: "Final", note: "Surfaces and borders fill in" },
];

const photo = (id: string, w: number, h: number) =>
  `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=${w}&h=${h}&q=80`;

export function BlueprintProcessDemo() {
  const scrollRef = React.useRef<HTMLDivElement>(null);
  const trackRef = React.useRef<HTMLDivElement>(null);
  const reduced = useReducedMotion();
  const { scrollYProgress } = useScroll({
    container: scrollRef,
    target: trackRef,
    offset: ["start start", "end end"],
  });
  const eased = useSpring(scrollYProgress, { stiffness: 300, damping: 50, restDelta: 0.0005 });
  // A beat on the wireframe at the top and on the finished page at the bottom.
  const progress = useTransform(reduced ? scrollYProgress : eased, [0.06, 0.94], [0, 1]);
  const hint = useTransform(scrollYProgress, [0, 0.04], [1, 0]);

  // The furthest stage any block has started in. Scrolling back drops blocks out again.
  const [stage, setStage] = React.useState(0);
  const started = React.useRef(new Map<HTMLElement, number>());
  const onBlockChange = React.useCallback((element: HTMLElement, state: BlueprintState) => {
    const map = started.current;
    if (state === "wireframe") map.delete(element);
    else map.set(element, Number(element.dataset.blueprintOrder ?? 0) + 1);
    setStage(Math.max(0, ...map.values()));
  }, []);

  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">
        <div ref={trackRef} className="relative h-[320%]">
          <div className="sticky top-0 flex h-[var(--demo-height,36rem)] flex-col gap-4 p-4 sm:flex-row sm:gap-6 sm:p-6">
            <Rail stage={stage} progress={progress} hint={hint} />

            <Blueprint
              play="manual"
              progress={progress}
              annotate
              onBlockChange={onBlockChange}
              skeletonClassName="text-white"
              className="min-h-0 flex-1 overflow-hidden rounded-xl bg-[#1f4f96] bg-[linear-gradient(to_right,rgb(255_255_255/0.07)_1px,transparent_1px),linear-gradient(to_bottom,rgb(255_255_255/0.07)_1px,transparent_1px)] bg-[size:24px_24px] p-4 sm:p-6"
            >
              <CaseStudy />
            </Blueprint>
          </div>
        </div>
      </div>
    </div>
  );
}

function Rail({
  stage,
  progress,
  hint,
}: {
  stage: number;
  progress: MotionValue<number>;
  hint: MotionValue<number>;
}) {
  return (
    <div className="flex shrink-0 flex-col justify-between sm:w-44">
      <div>
        <p className="font-mono text-[11px] tracking-widest text-zinc-500 uppercase">Case study 07</p>
        <p className="mt-1 hidden text-sm font-medium tracking-tight text-zinc-900 sm:block dark:text-zinc-50">
          From wireframe to launch
        </p>
      </div>

      <ol className="relative mt-3 flex gap-4 sm:mt-0 sm:flex-col sm:gap-6">
        <span className="absolute top-1.5 bottom-1.5 left-[3.5px] hidden w-px bg-zinc-200 sm:block dark:bg-zinc-800" />
        <motion.span
          className="absolute top-1.5 bottom-1.5 left-[3.5px] hidden w-px origin-top bg-zinc-900 sm:block dark:bg-zinc-100"
          style={{ scaleY: progress }}
        />
        {stages.map((item, i) => (
          <li key={item.title} className="relative flex items-start gap-2.5 sm:gap-3">
            <span
              className={cn(
                "relative mt-1 size-2 shrink-0 rounded-full ring-4 transition-colors duration-300",
                i <= stage
                  ? "bg-zinc-900 ring-white dark:bg-zinc-100 dark:ring-zinc-950"
                  : "bg-zinc-300 ring-white dark:bg-zinc-700 dark:ring-zinc-950",
              )}
            />
            <div>
              <p
                className={cn(
                  "text-xs font-medium transition-colors duration-300 sm:text-sm",
                  i === stage
                    ? "text-zinc-900 dark:text-zinc-50"
                    : i < stage
                      ? "text-zinc-500"
                      : "text-zinc-400 dark:text-zinc-600",
                )}
              >
                {item.title}
              </p>
              <p
                className={cn(
                  "mt-0.5 hidden text-xs leading-snug transition-colors duration-300 sm:block",
                  i === stage ? "text-zinc-500" : "text-zinc-400 dark:text-zinc-600",
                )}
              >
                {item.note}
              </p>
            </div>
          </li>
        ))}
      </ol>

      <motion.p
        style={{ opacity: hint }}
        className="hidden items-center gap-1.5 text-xs text-zinc-500 sm:flex"
      >
        Scroll to build
        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" className="size-3.5" aria-hidden>
          <path d="M8 3v10m0 0 4-4m-4 4-4-4" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </motion.p>
    </div>
  );
}

function CaseStudy() {
  return (
    <article
      data-blueprint="box"
      data-blueprint-order="2"
      data-blueprint-label="Page"
      className="flex h-full flex-col overflow-hidden rounded-lg bg-[#17130f] p-5 text-[#f1e7da] sm:p-6"
    >
      <header className="flex items-center justify-between gap-4">
        <span data-blueprint className="text-xs font-semibold tracking-[0.22em] uppercase">
          Ondas Coffee
        </span>
        <span data-blueprint className="font-mono text-[11px] text-[#f1e7da]/60">
          Brand · Web · 2026
        </span>
      </header>

      <div className="mt-7 grid gap-5 md:grid-cols-[1.1fr_1fr]">
        <div>
          <h2 data-blueprint className="text-2xl/tight font-semibold tracking-tight text-balance sm:text-3xl/tight">
            A roaster&apos;s site that smells like the shop
          </h2>
          <p data-blueprint className="mt-3 text-sm/6 text-pretty text-[#f1e7da]/70">
            We rebuilt Ondas around the Tuesday roast: one weekly drop, tasting notes from the roaster, and a
            checkout that takes under a minute.
          </p>
          <div className="mt-5 grid grid-cols-2 gap-3">
            <div
              data-blueprint="box"
              data-blueprint-order="2"
              className="rounded-lg border border-[#f1e7da]/15 bg-[#241d17] px-3 py-2.5"
            >
              <p data-blueprint data-blueprint-count className="text-2xl font-semibold tracking-tight tabular-nums">
                +48%
              </p>
              <p data-blueprint data-blueprint-label="" className="text-xs text-[#f1e7da]/60">
                online orders
              </p>
            </div>
            <div
              data-blueprint="box"
              data-blueprint-order="2"
              className="rounded-lg border border-[#f1e7da]/15 bg-[#241d17] px-3 py-2.5"
            >
              <p data-blueprint data-blueprint-count className="text-2xl font-semibold tracking-tight tabular-nums">
                3.1×
              </p>
              <p data-blueprint data-blueprint-label="" className="text-xs text-[#f1e7da]/60">
                repeat buyers
              </p>
            </div>
          </div>
        </div>
        <img
          data-blueprint
          data-blueprint-order="1"
          src={photo("1442512595331-e89e73853f31", 720, 540)}
          alt="Coffee dripping through a pour-over into a glass carafe"
          className="hidden aspect-[4/3] w-full rounded-lg object-cover md:block"
        />
      </div>

      <div className="mt-5 hidden min-h-24 flex-1 grid-cols-3 gap-3 sm:grid">
        <img
          data-blueprint
          data-blueprint-order="1"
          src={photo("1447933601403-0c6688de566e", 480, 300)}
          alt="Roasted coffee beans"
          className="h-full max-h-56 w-full rounded-lg object-cover"
        />
        <img
          data-blueprint
          data-blueprint-order="1"
          src={photo("1497935586351-b67a49e012bf", 480, 300)}
          alt="A latte, a pour-over and ground coffee on a wooden board"
          className="h-full max-h-56 w-full rounded-lg object-cover"
        />
        <a
          data-blueprint
          data-blueprint-order="2"
          href="#case-study"
          className="flex h-full max-h-56 flex-col justify-between rounded-lg bg-[#e8a35a] p-3 text-sm font-medium text-[#17130f]"
        >
          <span>Read the case study</span>
          <span aria-hidden>→</span>
        </a>
      </div>
    </article>
  );
}
```
