registry/blocks/hero/4/hero-section.tsx
"use client";
import { ArrowRightIcon } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import { ContentRail, SectionShell } from "@/components/layout-contract";
import { Button } from "@/components/ui/button";
import { GridPattern } from "@/components/ui/grid-pattern";
import { cn } from "@/lib/utils";
const spring = { type: "spring" as const, stiffness: 380, damping: 28 };
type HeadlineWord = {
text: string;
muted?: boolean;
accent?: boolean;
};
const headlineLines: HeadlineWord[][] = [
[{ text: "Preview" }, { text: "blocks" }, { text: "live." }],
[
{ text: "Install", muted: true },
{ text: "with" },
{ text: "shadcn.", accent: true },
],
];
function RevealWord({
word,
index,
reduceMotion,
}: {
word: HeadlineWord;
index: number;
reduceMotion: boolean;
}) {
return (
<span className="inline-flex overflow-hidden pb-[0.08em]">
<motion.span
className={cn(
word.muted && "text-muted-foreground",
word.accent && "text-primary",
)}
initial={
reduceMotion
? false
: { opacity: 0, y: "100%", filter: "blur(4px)" }
}
transition={{ type: "spring", duration: 0.35, bounce: 0, delay: reduceMotion ? 0 : 0.1 + index * 0.07 }}
viewport={{ once: true, margin: "-5%" }}
whileInView={reduceMotion ? undefined : { opacity: 1, y: 0, filter: "blur(0px)" }}
>
{word.text}
</motion.span>
</span>
);
}
export function HeroWordRevealSection() {
const reduceMotion = useReducedMotion();
return (
<SectionShell spacingMode="section" className="relative isolate flex min-h-[min(88vh,52rem)] flex-col justify-center overflow-hidden">
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
<div className="absolute top-1/2 left-1/2 h-[min(100vw,720px)] w-[min(100vw,720px)] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/[0.05] blur-3xl" />
<GridPattern
className="absolute inset-0 mask-[radial-gradient(ellipse_at_center,white,transparent_68%)] stroke-foreground/[0.05]"
height={56}
width={56}
/>
</div>
<ContentRail>
<motion.p
className="text-pretty text-muted-foreground text-sm lowercase tracking-wide"
initial={reduceMotion ? false : { opacity: 0, y: 8, filter: "blur(4px)" }}
transition={spring}
viewport={{ once: true }}
whileInView={reduceMotion ? undefined : { opacity: 1, y: 0, filter: "blur(0px)" }}
>
open registry
</motion.p>
<h1 className="mt-6 max-w-5xl font-medium text-[clamp(2.25rem,6.5vw,4.75rem)] leading-[1.02] tracking-[-0.04em]">
{headlineLines.map((line, lineIndex) => {
let wordOffset = 0;
for (let i = 0; i < lineIndex; i++) {
wordOffset += headlineLines[i]!.length;
}
return (
<span
className="flex flex-wrap items-baseline gap-x-[0.28em]"
key={line.map((word) => word.text).join("-")}
>
{line.map((word, wordIndex) => (
<RevealWord
index={wordOffset + wordIndex}
key={`${word.text}-${wordIndex}`}
reduceMotion={!!reduceMotion}
word={word}
/>
))}
</span>
);
})}
</h1>
<motion.p
className="mt-8 max-w-md text-pretty text-muted-foreground text-sm leading-relaxed md:text-base"
initial={reduceMotion ? false : { opacity: 0, y: 10 }}
transition={{ ...spring, delay: 0.5 }}
viewport={{ once: true }}
whileInView={reduceMotion ? undefined : { opacity: 1, y: 0 }}
>
Browse motion-first sections, copy prompts, and add components to your
project — without rebuilding the same layout twice.
</motion.p>
<motion.div
className="mt-10 flex flex-wrap gap-3"
initial={reduceMotion ? false : { opacity: 0, y: 10 }}
transition={{ ...spring, delay: 0.58 }}
viewport={{ once: true }}
whileInView={reduceMotion ? undefined : { opacity: 1, y: 0 }}
>
<Button className="active:scale-[0.96] transition-transform">
Browse blocks
<ArrowRightIcon data-icon="inline-end" />
</Button>
<Button className="active:scale-[0.96] transition-transform" variant="outline">
View pricing
</Button>
</motion.div>
</ContentRail>
</SectionShell>
);
}





























