# Echo Reel: Plan Picker

> The giant word picks a pricing plan: click the proof above or below, or use the arrow keys, and the price card beside it changes.

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-plans-demo).

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

## Install

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

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/echo-reel#echo-reel-plans-demo

## Files

### `registry/demos/echo-reel-plans-demo.tsx`

```tsx
"use client";

import * as React from "react";

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

const PLANS = [
  {
    name: "Solo",
    price: 12,
    blurb: "For one brand you're building yourself.",
    lines: [
      "One brand kit",
      "Logo, palette and type pairings",
      "Export to Figma and PDF",
    ],
    cta: "Start with Solo",
  },
  {
    name: "Team",
    price: 36,
    blurb: "For an in-house team keeping five brands in line.",
    lines: [
      "Five brand kits, five seats",
      "Shared templates and review links",
      "Version history on every asset",
    ],
    cta: "Try Team free for 14 days",
  },
  {
    name: "Studio",
    price: 96,
    blurb: "For agencies handing kits to clients.",
    lines: [
      "Unlimited client kits, 20 seats",
      "White-label handoff pages",
      "Priority support and SSO",
    ],
    cta: "Talk to us about Studio",
  },
];

function Check() {
  return (
    <svg
      viewBox="0 0 16 16"
      aria-hidden
      className="mt-0.5 size-4 shrink-0 text-[#ff3d1f] dark:text-[#ff6a4d]"
    >
      <path
        d="M3.5 8.5l3 3 6-7"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.75"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

export function EchoReelPlansDemo() {
  const [plan, setPlan] = React.useState(1);

  return (
    <section className="@container w-full max-w-4xl overflow-hidden rounded-3xl border border-[#0b0b0d]/10 bg-white px-5 pt-8 pb-8 text-[#0b0b0d] @[44rem]:px-10 @[44rem]:pb-10 dark:border-white/10 dark:bg-[#08080b] dark:text-[#f4f4f6]">
      <div className="flex flex-wrap items-baseline justify-between gap-x-6 gap-y-1">
        <h2 className="text-lg font-semibold tracking-tight">Tinta pricing</h2>
        <p className="text-[13px] text-[#0b0b0d]/55 dark:text-[#f4f4f6]/55">
          Click a plan above or below, or use <kbd className="font-sans">↑</kbd>{" "}
          <kbd className="font-sans">↓</kbd>
        </p>
      </div>

      <div className="mt-6 grid items-center gap-6 @[44rem]:mt-8 @[44rem]:grid-cols-[minmax(0,1.15fr)_minmax(0,1fr)] @[44rem]:gap-10">
        <EchoReel
          words={PLANS.map((p) => p.name)}
          value={plan}
          onValueChange={(index) => setPlan(index)}
          interval={false}
          aria-label="Plan"
          className="mx-auto max-w-md text-[#ff3d1f] dark:text-[#ff5a3a]"
        />

        {/* Every plan in one grid cell, so the card keeps its height while they cross. */}
        <div className="mx-auto grid w-full max-w-sm">
          {PLANS.map((p, i) => {
            const on = i === plan;
            return (
              <div
                key={p.name}
                aria-hidden={!on}
                inert={!on}
                className="[grid-area:1/1] rounded-2xl border border-[#0b0b0d]/10 bg-white p-5 shadow-sm transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-opacity dark:border-white/10 dark:bg-white/[0.04]"
                style={{
                  opacity: on ? 1 : 0,
                  transform: on ? "none" : `translateY(${i < plan ? -6 : 6}px)`,
                  transitionDelay: on ? "120ms" : "0ms",
                }}
              >
                <div className="flex items-baseline gap-1.5">
                  <span className="text-4xl font-semibold tracking-tight tabular-nums">
                    ${p.price}
                  </span>
                  <span className="text-sm text-[#0b0b0d]/55 dark:text-[#f4f4f6]/55">
                    a month
                  </span>
                </div>
                <p className="mt-1 text-sm text-[#0b0b0d]/65 dark:text-[#f4f4f6]/65">
                  {p.blurb}
                </p>
                <ul className="mt-4 space-y-2 text-sm">
                  {p.lines.map((line) => (
                    <li key={line} className="flex gap-2">
                      <Check />
                      {line}
                    </li>
                  ))}
                </ul>
                <a
                  href="#plan"
                  tabIndex={on ? undefined : -1}
                  className="mt-5 flex h-10 items-center justify-center rounded-full bg-[#ff3d1f] text-sm font-semibold text-white transition-colors duration-150 hover:bg-[#e8331a] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ff3d1f] dark:bg-[#ff5a3a] dark:text-[#0b0b0d] dark:hover:bg-[#ff6e52]"
                >
                  {p.cta}
                </a>
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
}
```
