# Pricing 5

> Horizontal scroll-snap filmstrip of plan cards — active tier scales up while neighbors recede (blog-5 snap-carousel DNA).

A shadcn/ui block from the [@nusaiba](https://nusaiba.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/nusaiba/pricing-5).

- Page, with a live preview: https://shadcn.exchange/r/nusaiba/pricing-5
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/nusaiba/pricing-5-light-f7e5366758b2fb54.png
- Registry item: https://nusaiba.dev/r/base-nova/pricing-5.json
- Categories: pricing
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://nusaiba.dev/r/base-nova/pricing-5.json
```

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

## Dependencies

- npm packages: `motion`
- Registry items: `@nusaiba/foundation`, `button`

## Files

### `registry/blocks/pricing/5/pricing-section.tsx`

```tsx
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { CheckIcon } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { SectionShell, ContentRail } from "@/components/layout-contract";

const plans = [
	{
		name: "Starter",
		tag: "free",
		price: "$0",
		period: "forever",
		features: ["Free blocks", "CLI install", "Copy prompt"],
	},
	{
		name: "Pro",
		tag: "once",
		price: "$147",
		period: "lifetime",
		features: ["Pro catalog", "Registry token", "Commercial license"],
		popular: true,
	},
	{
		name: "Enterprise",
		tag: "team",
		price: "Custom",
		period: "annual",
		features: ["Team workspace", "SSO", "Priority support"],
	},
];

export function PricingFilmstripSection() {
	const reduceMotion = useReducedMotion();
	const scrollRef = useRef<HTMLDivElement>(null);
	const slideRefs = useRef<(HTMLElement | null)[]>([]);
	const [active, setActive] = useState(1);

	const updateActive = useCallback(() => {
		const container = scrollRef.current;
		if (!container) return;

		const center = container.scrollLeft + container.clientWidth / 2;
		let closest = 0;
		let minDistance = Number.POSITIVE_INFINITY;

		slideRefs.current.forEach((slide, index) => {
			if (!slide) return;
			const slideCenter = slide.offsetLeft + slide.offsetWidth / 2;
			const distance = Math.abs(center - slideCenter);
			if (distance < minDistance) {
				minDistance = distance;
				closest = index;
			}
		});

		setActive(closest);
	}, []);

	useEffect(() => {
		const container = scrollRef.current;
		if (!container) return;

		updateActive();
		container.addEventListener("scroll", updateActive, { passive: true });
		window.addEventListener("resize", updateActive);
		return () => {
			container.removeEventListener("scroll", updateActive);
			window.removeEventListener("resize", updateActive);
		};
	}, [updateActive]);

	return (
		<SectionShell spacingMode="section" className="bg-background">
			<ContentRail maxWidth="max-w-6xl" className="space-y-8 md:space-y-10">
				<div className="max-w-xl space-y-2">
					<p className="font-medium text-primary text-xs uppercase tracking-[0.22em]">
						Plans
					</p>
					<h2 className="text-balance font-medium text-2xl tracking-tight md:text-4xl">
						Swipe to compare
					</h2>
					<p className="text-pretty text-muted-foreground text-sm md:text-base">
						Scroll-snap filmstrip — active tier scales up while neighbors recede.
					</p>
				</div>

				<div
					className="flex snap-x snap-mandatory gap-4 overflow-x-auto pb-2 [-ms-overflow-style:none] [scrollbar-width:none] md:gap-6 [&::-webkit-scrollbar]:hidden"
					ref={scrollRef}
				>
					{plans.map((plan, index) => {
						const isActive = index === active;

						return (
							<motion.article
								animate={
									reduceMotion
										? undefined
										: { scale: isActive ? 1 : 0.94, opacity: isActive ? 1 : 0.55 }
								}
								className="w-[min(88vw,22rem)] shrink-0 snap-center"
								initial={reduceMotion ? false : { opacity: 0, y: 16 }}
								key={plan.name}
								ref={(node) => {
									slideRefs.current[index] = node;
								}}
								transition={{ type: "spring", duration: 0.35, bounce: 0 }}
								viewport={{ once: true }}
								whileInView={reduceMotion ? undefined : { opacity: 1, y: 0 }}
							>
								<div
									className={cn(
										"flex h-full flex-col rounded-[var(--radius-md)] border p-6 shadow-[0_1px_2px_rgba(0,0,0,0.04)] md:p-7 dark:shadow-[0_1px_2px_rgba(0,0,0,0.2)]",
										plan.popular
											? "border-foreground/12 bg-gradient-to-b from-primary/[0.06] to-background"
											: "border-border bg-background",
									)}
								>
									<div className="flex items-center justify-between gap-2">
										<p className="font-medium text-lg">{plan.name}</p>
										<span className="font-mono text-[10px] text-muted-foreground uppercase tracking-[0.14em]">
											{plan.tag}
										</span>
									</div>
									<div className="mt-4 flex items-baseline gap-2">
										<span className="font-medium text-4xl tabular-nums tracking-tight">
											{plan.price}
										</span>
										<span className="text-muted-foreground text-sm">{plan.period}</span>
									</div>
									<ul className="mt-6 flex-1 space-y-2.5">
										{plan.features.map((feature) => (
											<li className="flex items-center gap-2 text-sm" key={feature}>
												<CheckIcon className="size-4 shrink-0 text-primary" />
												{feature}
											</li>
										))}
									</ul>
									<Button
										className="mt-8 w-full active:scale-[0.96]"
										variant={plan.popular ? "default" : "outline"}
									>
										{plan.price === "Custom" ? "Talk to us" : "Get started"}
									</Button>
								</div>
							</motion.article>
						);
					})}
				</div>
			</ContentRail>
		</SectionShell>
	);
}
```
