Free vs Pro fork with center-stage focus shift — pricing-2 DNA for registry path choice.
@nusaiba
registry/blocks/cta/6/cta-section.tsx
"use client";
import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react";
import { ArrowRightIcon, CheckIcon } from "lucide-react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { buttonVariants } from "@/components/ui/button";
import { motionStagger, motionViewport } from "@/lib/motion-tokens";
import { cn } from "@/lib/utils";
import { SectionShell } from "@/components/layout-contract";
const spring = { type: "spring" as const, duration: 0.3, bounce: 0 };
const CYCLE_MS = 3600;
const paths = [
{
id: "free",
eyebrow: "Open registry",
title: "@nusaiba",
points: ["Free blocks", "Live preview", "Copy prompt"],
cta: "Browse free",
href: "/blocks",
variant: "outline" as const,
},
{
id: "pro",
eyebrow: "Pro registry",
title: "@nusaiba-pro",
points: ["Full catalog", "Registry token", "Commercial license"],
cta: "Get Pro access",
href: "/pricing",
variant: "default" as const,
popular: true,
},
] as const;
export function CtaDualPathSection() {
const reduceMotion = useReducedMotion();
const ref = useRef<HTMLElement>(null);
const inView = useInView(ref, motionViewport);
const [active, setActive] = useState<"free" | "pro">("pro");
const [hasFocus, setHasFocus] = useState(false);
const activate = useCallback((id: "free" | "pro") => {
setActive(id);
setHasFocus(true);
}, []);
const deactivate = useCallback(() => setHasFocus(false), []);
useEffect(() => {
if (reduceMotion || hasFocus || !inView) return;
const timer = setInterval(() => {
setActive((current) => (current === "pro" ? "free" : "pro"));
}, CYCLE_MS);
return () => clearInterval(timer);
}, [hasFocus, inView, reduceMotion]);
const itemVariants = reduceMotion
? { hidden: { opacity: 1 }, visible: { opacity: 1 } }
: {
hidden: { opacity: 0, filter: "blur(4px)", y: 8 },
visible: { opacity: 1, filter: "blur(0px)", y: 0, transition: spring },
};
return (
<SectionShell spacingMode="section" ref={ref}>
<motion.div
animate={inView ? "visible" : "hidden"}
className="mx-auto w-full max-w-4xl"
initial="hidden"
variants={{
hidden: {},
visible: { transition: { staggerChildren: motionStagger.item, delayChildren: 0.04 } },
}}
>
<motion.div className="mx-auto max-w-xl text-center" variants={itemVariants}>
<p className="font-medium text-primary text-xs uppercase tracking-[0.22em]">Choose</p>
<h2 className="mt-3 text-balance font-medium text-3xl tracking-tight md:text-4xl">
Pick your install path
</h2>
<p className="mt-4 text-pretty text-muted-foreground text-sm md:text-base">
Start with open blocks or unlock the full motion catalog with a registry
token.
</p>
</motion.div>
<motion.div
className="mt-10 grid gap-4 sm:grid-cols-2"
variants={itemVariants}
>
{paths.map((path) => {
const isActive = active === path.id;
const dimmed = hasFocus ? !isActive : !isActive;
return (
<div
className={cn(
"relative rounded-2xl border p-6 transition-[border-color,box-shadow,transform,opacity,filter] md:p-8",
"shadow-[0_1px_0_rgba(0,0,0,0.04)]",
isActive
? "border-foreground/15 bg-muted/20"
: "border-border bg-background",
dimmed && !reduceMotion && "opacity-70 blur-[0.5px]",
)}
key={path.id}
onBlur={deactivate}
onFocus={() => activate(path.id)}
onMouseEnter={() => activate(path.id)}
onMouseLeave={deactivate}
>
{"popular" in path && path.popular ? (
<span className="absolute top-4 right-4 rounded-full border border-border bg-background px-2 py-0.5 font-medium text-[10px] text-muted-foreground uppercase tracking-wide">
Popular
</span>
) : null}
<p className="font-medium text-primary text-xs uppercase tracking-[0.18em]">
{path.eyebrow}
</p>
<p className="mt-2 font-medium font-mono text-xl tracking-tight">
{path.title}
</p>
<ul className="mt-5 space-y-2">
{path.points.map((point) => (
<li className="flex items-center gap-2 text-muted-foreground text-sm" key={point}>
<CheckIcon className="size-3.5 shrink-0 text-foreground" />
{point}
</li>
))}
</ul>
<Link
className={cn(
buttonVariants({ variant: path.variant }),
"mt-6 w-full active:scale-[0.96] transition-transform",
)}
href={path.href}
>
{path.cta}
<ArrowRightIcon data-icon="inline-end" />
</Link>
</div>
);
})}
</motion.div>
</motion.div>
</SectionShell>
);
}





























