2×3 interactive capability grid with auto-cycle pulse focus — hover overrides cycle, label expands on active cell (logo-cloud-3 DNA).
@nusaiba
registry/blocks/features/5/features-pulse-section.tsx
"use client";
import { useCallback, useEffect, useState } from "react";
import type { LucideIcon } from "lucide-react";
import {
ClipboardCopyIcon,
EyeIcon,
KeyRoundIcon,
SparklesIcon,
SquareTerminalIcon,
UnlockIcon,
} from "lucide-react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { motionStagger, motionViewport } from "@/lib/motion-tokens";
import { cn } from "@/lib/utils";
import { SectionShell, ContentRail } from "@/components/layout-contract";
const CYCLE_MS = 3400;
type Capability = {
id: string;
title: string;
short: string;
description: string;
icon: LucideIcon;
};
const capabilities: Capability[] = [
{
id: "motion",
title: "Motion docs",
short: "Motion",
description: "40 live patterns linked to blocks — spring tokens, triggers, and isolated demos.",
icon: SparklesIcon,
},
{
id: "cli",
title: "CLI install",
short: "CLI",
description: "npx shadcn add @nusaiba/block-name copies source into your repo with your tokens.",
icon: SquareTerminalIcon,
},
{
id: "pro",
title: "Pro blocks",
short: "Pro",
description: "Depth sections for pricing, integrations, and motion-heavy layouts beyond the free catalog.",
icon: UnlockIcon,
},
{
id: "prompt",
title: "Copy prompt",
short: "Prompt",
description: "Every block page ships an AI-ready install prompt when you prefer that workflow.",
icon: ClipboardCopyIcon,
},
{
id: "preview",
title: "Preview live",
short: "Preview",
description: "Inspect motion and layout on the site before anything lands in your codebase.",
icon: EyeIcon,
},
{
id: "token",
title: "Token auth",
short: "Token",
description: "Private registry token for Pro installs — no subscription lock-in on solo use.",
icon: KeyRoundIcon,
},
];
function PulseFeatureCell({
capability,
isActive,
hasFocus,
onActivate,
onDeactivate,
reduceMotion,
cellVariants,
}: {
capability: Capability;
isActive: boolean;
hasFocus: boolean;
onActivate: () => void;
onDeactivate: () => void;
reduceMotion: boolean;
cellVariants: {
hidden: { opacity: number; scale?: number; filter?: string };
visible: { opacity: number; scale?: number; filter?: string };
};
}) {
const Icon = capability.icon;
const dimmed = hasFocus ? !isActive : false;
const pulsing = isActive && !hasFocus && !reduceMotion;
return (
<motion.button
aria-label={capability.title}
aria-pressed={isActive}
className={cn(
"group relative flex min-h-[8.5rem] w-full flex-col items-start justify-between rounded-[var(--radius-md)] border border-border bg-background p-4 text-left transition-[background-color,border-color,transform,box-shadow] active:scale-[0.96] md:min-h-[9rem] md:p-5",
"after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-1/2",
isActive &&
"border-foreground/10 bg-gradient-to-b from-primary/[0.05] to-background shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:shadow-[0_1px_2px_rgba(0,0,0,0.2)]",
)}
onBlur={onDeactivate}
onFocus={onActivate}
onMouseEnter={onActivate}
onMouseLeave={onDeactivate}
type="button"
variants={cellVariants}
whileHover={reduceMotion ? undefined : { y: -2 }}
>
<motion.div
animate={
reduceMotion
? undefined
: dimmed
? { opacity: 0.42, scale: 0.98, filter: "blur(1px)" }
: pulsing
? { opacity: 1, scale: [1, 1.02, 1], filter: "blur(0px)" }
: { opacity: isActive ? 1 : 0.62, scale: 1, filter: "blur(0px)" }
}
className="flex w-full flex-col gap-3"
transition={
pulsing
? { duration: 2.4, repeat: Number.POSITIVE_INFINITY, ease: "easeInOut" }
: { type: "spring", duration: 0.32, bounce: 0 }
}
>
<div className="flex items-center gap-2.5">
<span
className={cn(
"inline-flex size-9 items-center justify-center rounded-[var(--radius-sm)] border border-border bg-muted/30",
isActive && "border-foreground/10 bg-primary/[0.08] text-primary",
)}
>
<Icon className="size-4" strokeWidth={2} />
</span>
<div className="min-w-0">
<p
className={cn(
"font-medium text-sm transition-[opacity,transform]",
isActive ? "text-foreground" : "text-foreground/75",
)}
>
{isActive ? capability.title : capability.short}
</p>
{!isActive ? (
<p className="truncate font-mono text-[10px] text-muted-foreground uppercase tracking-[0.12em]">
{capability.title}
</p>
) : null}
</div>
</div>
<motion.p
animate={
reduceMotion
? undefined
: isActive
? { opacity: 1, height: "auto", filter: "blur(0px)" }
: { opacity: 0, height: 0, filter: "blur(4px)" }
}
className="overflow-hidden text-pretty text-muted-foreground text-sm leading-relaxed"
transition={{ type: "spring", duration: 0.3, bounce: 0 }}
>
{capability.description}
</motion.p>
</motion.div>
</motion.button>
);
}
export function FeaturesPulseGridSection() {
const reduceMotion = useReducedMotion();
const [rootNode, setRootNode] = useState<HTMLElement | null>(null);
const isInView = useInView({ current: rootNode }, { ...motionViewport, amount: 0.3 });
const [active, setActive] = useState(0);
const [cycleActive, setCycleActive] = useState(false);
const [hoverFocus, setHoverFocus] = useState<string | null>(null);
useEffect(() => {
if (!isInView || reduceMotion) return;
setCycleActive(true);
}, [isInView, reduceMotion]);
useEffect(() => {
if (!cycleActive || reduceMotion || hoverFocus) return;
const timer = setInterval(() => {
setActive((current) => (current + 1) % capabilities.length);
}, CYCLE_MS);
return () => clearInterval(timer);
}, [cycleActive, hoverFocus, reduceMotion]);
const selectCapability = useCallback((id: string, index: number) => {
setHoverFocus(id);
setActive(index);
setCycleActive(false);
}, []);
const clearFocus = useCallback((id: string) => {
setHoverFocus((current) => (current === id ? null : current));
setCycleActive(true);
}, []);
const cellVariants = reduceMotion
? { hidden: { opacity: 1 }, visible: { opacity: 1 } }
: {
hidden: { opacity: 0, scale: 0.97, filter: "blur(4px)" },
visible: { opacity: 1, scale: 1, filter: "blur(0px)" },
};
return (
<SectionShell spacingMode="section" className="bg-background" ref={setRootNode}>
<ContentRail maxWidth="max-w-4xl" className="space-y-8 md:space-y-10">
<div className="max-w-xl space-y-2 text-left">
<p className="font-medium text-primary text-xs uppercase tracking-[0.22em]">
Capabilities
</p>
<h2 className="text-balance font-medium text-2xl tracking-tight md:text-4xl">
What you get with the registry
</h2>
<p className="text-pretty text-muted-foreground text-sm md:text-base">
Six core workflows — one cell in focus at a time. Hover to override the
auto-cycle pulse.
</p>
</div>
<motion.ul
className="grid gap-3 sm:grid-cols-2 md:gap-4"
initial="hidden"
variants={{
hidden: {},
visible: {
transition: {
staggerChildren: motionStagger.item,
delayChildren: 0.04,
},
},
}}
animate={isInView ? "visible" : "hidden"}
>
{capabilities.map((capability, index) => (
<motion.li key={capability.id} variants={cellVariants}>
<PulseFeatureCell
capability={capability}
cellVariants={cellVariants}
hasFocus={hoverFocus !== null}
isActive={index === active}
onActivate={() => selectCapability(capability.id, index)}
onDeactivate={() => clearFocus(capability.id)}
reduceMotion={!!reduceMotion}
/>
</motion.li>
))}
</motion.ul>
</ContentRail>
</SectionShell>
);
}



























