# Echo Reel Pricing

> A pricing section where the plan picker is one giant word. Click the plan printed above or below, or press ↑ ↓, and the reel turns it into the middle and inks it in, while the card beside it rolls the price and the monthly photo count with an Odometer. A monthly or yearly switch rolls the price too.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/echo-reel-pricing).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/echo-reel-pricing
- Screenshot: https://shadcn.exchange/shot/keyframeui/echo-reel-pricing/light.png
- Registry item: https://keyframeui.com/r/echo-reel-pricing.json
- Categories: blocks, pricing-sections
- Listed: 2026-11-02, source last changed 2026-11-02

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/echo-reel-pricing.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/echo-reel.json`, `https://keyframeui.com/r/odometer.json`

## Notes from the registry

Live preview and the components it's built with: https://keyframeui.com/blocks/pricing-sections/echo-reel-pricing

## Files

### `registry/blocks/echo-reel-pricing.tsx`

```tsx
"use client";

import * as React from "react";

import { EchoReel } from "@/components/ui/echo-reel";
import { Odometer } from "@/components/ui/odometer";

/* ── Content ─────────────────────────────────────────────────
 * Everything you'd change to make the section yours: the
 * palette, the plans and the copy. The giant word is the plan
 * picker: click the word above or below, or focus it and use
 * the arrow keys, and the card beside it rolls to that plan.
 * ─────────────────────────────────────────────────────────── */

const PALETTE = {
  light: { paper: "#ffffff", ink: "#0b0b10", accent: "#5b2bff", card: "#f6f5fb" },
  dark: { paper: "#07070c", ink: "#f5f5f7", accent: "#9b83ff", card: "#111018" },
};

const EYEBROW = "Pricing";
const HEADLINE = "Pay for photos, not seats.";
const HINT = "Pick a plan on the reel, or focus it and press ↑ ↓";
const YEARLY_SAVING = 0.2;

const PLANS = [
  {
    name: "Starter",
    monthly: 19,
    photos: 60,
    blurb: "For a shop with a few new products a month.",
    features: ["Cutouts and soft shadows", "Every marketplace crop", "Alt text in English", "Exports up to 2K", "Email support"],
    cta: { label: "Start with Starter", href: "#starter" },
  },
  {
    name: "Studio",
    monthly: 79,
    photos: 400,
    blurb: "For brands shooting a full catalog every season.",
    features: ["Everything in Starter", "Lifestyle scenes, lit to match", "12 colorways from one shot", "Alt text in 9 languages", "4K exports for print"],
    cta: { label: "Try Studio free for 14 days", href: "#studio" },
  },
  {
    name: "Brand",
    monthly: 249,
    photos: 2000,
    blurb: "For teams feeding a store, ads and wholesale at once.",
    features: ["Everything in Studio", "Your own trained look", "Bulk uploads from a folder", "Five seats, review links", "A shoot lead on Slack"],
    cta: { label: "Talk to us about Brand", href: "#brand" },
  },
];

/* ── Section ─────────────────────────────────────────────── */

const usd: Intl.NumberFormatOptions = { style: "currency", currency: "USD", maximumFractionDigits: 0 };

export function EchoReelPricing() {
  const [plan, setPlan] = React.useState(1);
  const [yearly, setYearly] = React.useState(true);
  const current = PLANS[plan];
  const price = Math.round(current.monthly * (yearly ? 1 - YEARLY_SAVING : 1));

  return (
    <section
      style={
        {
          "--paper-light": PALETTE.light.paper,
          "--ink-light": PALETTE.light.ink,
          "--accent-light": PALETTE.light.accent,
          "--card-light": PALETTE.light.card,
          "--paper-dark": PALETTE.dark.paper,
          "--ink-dark": PALETTE.dark.ink,
          "--accent-dark": PALETTE.dark.accent,
          "--card-dark": PALETTE.dark.card,
        } as React.CSSProperties
      }
      className="relative overflow-hidden bg-(--paper) text-(--ink) [--accent:var(--accent-light)] [--card:var(--card-light)] [--ink:var(--ink-light)] [--paper:var(--paper-light)] dark:[--accent:var(--accent-dark)] dark:[--card:var(--card-dark)] dark:[--ink:var(--ink-dark)] dark:[--paper:var(--paper-dark)]"
    >
      <div className="mx-auto max-w-6xl px-6 py-24 sm:py-28">
        <div className="flex flex-col items-start justify-between gap-6 sm:flex-row sm:items-end">
          <div>
            <p className="text-sm font-semibold text-(--accent)">{EYEBROW}</p>
            <h2 className="mt-3 text-4xl font-semibold tracking-[-0.04em] text-balance sm:text-5xl">{HEADLINE}</h2>
          </div>
          <div role="group" aria-label="Billing" className="flex rounded-full bg-(--card) p-1 text-sm font-medium">
            {[false, true].map((y) => (
              <button
                key={String(y)}
                type="button"
                aria-pressed={yearly === y}
                onClick={() => setYearly(y)}
                className="flex h-9 items-center gap-1.5 rounded-full px-4 transition-[background-color,color,box-shadow] duration-200 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) aria-pressed:bg-(--paper) aria-pressed:shadow-[0_1px_2px_rgb(0_0_0/0.08),0_0_0_1px_rgb(0_0_0/0.04)] [&:not([aria-pressed=true])]:opacity-60"
              >
                {y ? "Yearly" : "Monthly"}
                {y && <span className="rounded-full bg-(--accent) px-1.5 py-0.5 text-[10px] leading-none font-semibold text-white dark:text-[#07070c]">−20%</span>}
              </button>
            ))}
          </div>
        </div>

        <div className="mt-12 grid items-center gap-10 lg:mt-16 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)] lg:gap-14">
          <div>
            <EchoReel
              words={PLANS.map((p) => p.name)}
              value={plan}
              onValueChange={(index) => setPlan(index)}
              interval={false}
              className="text-(--accent)"
            />
            <p className="mt-4 text-center text-[13px] opacity-55">{HINT}</p>
          </div>

          <div className="rounded-[28px] bg-(--card) p-7 sm:p-8">
            <div className="flex items-baseline justify-between gap-4">
              <p className="text-sm font-semibold">{current.name}</p>
              <p className="text-sm opacity-60">
                <Odometer value={current.photos} className="font-semibold tabular-nums opacity-100" /> photos a month
              </p>
            </div>
            <p className="mt-5 flex items-baseline gap-1.5">
              <Odometer value={price} format={usd} className="text-5xl font-semibold tracking-[-0.04em]" />
              <span className="text-sm opacity-60">/month</span>
            </p>
            <p className="mt-1.5 text-[13px] opacity-60">
              {yearly ? `Billed $${(price * 12).toLocaleString("en-US")} a year` : "Billed monthly, cancel anytime"}
            </p>
            <p className="mt-5 text-[15px] leading-relaxed opacity-80">{current.blurb}</p>
            <ul className="mt-5 space-y-2.5 text-[14px]">
              {current.features.map((feature) => (
                <li key={feature} className="flex gap-2.5">
                  <svg viewBox="0 0 16 16" aria-hidden className="mt-0.5 size-4 shrink-0 text-(--accent)">
                    <path d="M3.5 8.5l3 3 6-7" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
                  </svg>
                  {feature}
                </li>
              ))}
            </ul>
            <a
              href={current.cta.href}
              className="mt-7 flex h-12 items-center justify-center rounded-full bg-(--accent) px-6 text-[15px] font-semibold text-white transition-[opacity,scale] duration-150 hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) active:scale-[0.98] dark:text-[#07070c]"
            >
              {current.cta.label}
            </a>
          </div>
        </div>
      </div>
    </section>
  );
}
```
