registry/blocks/blog/9/blog-section.tsx
"use client";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
import { ArrowRightIcon, ClockIcon } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/lib/utils";
import { SectionShell } from "@/components/layout-contract";
const spring = { type: "spring" as const, stiffness: 380, damping: 28 };
const posts = [
{
title: "Motion tokens for marketing pages",
excerpt: "Spring stiffness, stagger, and whileInView thresholds that feel authored.",
category: "Craft",
readTime: "7 min",
image: "/images/visuals/pixel-blast.png",
alt: "Glowing pixel clusters on black",
},
{
title: "Designing onboarding that converts",
excerpt: "Five patterns that activate users in the first session.",
category: "Design",
readTime: "5 min",
image: "/images/visuals/orb.png",
alt: "Glowing orbital ring",
},
{
title: "Scroll stacks without jank",
excerpt: "Sticky cards, spring handoff, pause on hover.",
category: "Motion",
readTime: "6 min",
image: "/images/visuals/hyperspeed.png",
alt: "Neon light trails",
},
{
title: "Building a registry developers install",
excerpt: "Preview live, copy prompts, CLI paths.",
category: "Product",
readTime: "9 min",
image: "/images/visuals/plasma.png",
alt: "Plasma smoke field",
},
];
const marqueeTitles = [...posts, ...posts].map((post) => post.title);
export function BlogMarqueeSection() {
const [active, setActive] = useState(0);
const [paused, setPaused] = useState(false);
const activePost = posts[active]!;
useEffect(() => {
if (paused) return;
const timer = setInterval(() => {
setActive((current) => (current + 1) % posts.length);
}, 4800);
return () => clearInterval(timer);
}, [paused]);
return (
<SectionShell spacingMode="section" className="relative isolate overflow-hidden">
<div
className="border-y border-border bg-muted/20 py-4"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
<div className="flex overflow-hidden">
<motion.div
animate={{ x: ["0%", "-50%"] }}
className="flex shrink-0 gap-8 pr-8"
transition={{
duration: 28,
ease: "linear",
repeat: Number.POSITIVE_INFINITY,
}}
>
{marqueeTitles.map((title, index) => (
<button
className={cn(
"shrink-0 font-medium text-sm tracking-tight transition-colors md:text-base",
posts[index % posts.length]?.title === posts[active]?.title
? "text-foreground"
: "text-muted-foreground hover:text-foreground/80",
)}
key={`${title}-${index}`}
onClick={() => setActive(index % posts.length)}
type="button"
>
{title}
</button>
))}
</motion.div>
</div>
</div>
<div className="mx-auto mt-12 w-full max-w-3xl md:mt-14">
<div className="mb-8 max-w-xl">
<p className="font-medium text-primary text-xs uppercase tracking-[0.25em]">
From the journal
</p>
<h2 className="mt-3 font-medium text-2xl tracking-tight md:text-4xl">
Marquee with a featured story
</h2>
</div>
<AnimatePresence mode="wait">
<motion.article
animate={{ opacity: 1, y: 0 }}
className="overflow-hidden rounded-[var(--radius-lg)] border border-border bg-card shadow-sm"
exit={{ opacity: 0, y: -10 }}
initial={{ opacity: 0, y: 14 }}
key={activePost.title}
transition={spring}
>
<div className="relative aspect-[16/9] overflow-hidden">
<Image
alt={activePost.alt}
className="object-cover"
fill
priority
sizes="(max-width: 768px) 100vw, 48rem"
src={activePost.image}
/>
<div className="absolute inset-0 bg-linear-to-t from-foreground/70 via-foreground/10 to-transparent" />
<div className="absolute inset-x-0 bottom-0 p-6 md:p-8">
<p className="font-medium text-white/75 text-xs uppercase tracking-[0.16em]">
{activePost.category}
</p>
<h3 className="mt-2 text-balance font-medium text-white text-2xl tracking-tight md:text-3xl">
{activePost.title}
</h3>
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-4 p-6 md:p-8">
<div className="max-w-lg">
<p className="text-muted-foreground text-sm leading-relaxed md:text-base">
{activePost.excerpt}
</p>
<p className="mt-3 inline-flex items-center gap-1 text-muted-foreground text-xs">
<ClockIcon className="size-3" />
{activePost.readTime}
</p>
</div>
<Link
className="group inline-flex items-center gap-1.5 font-medium text-sm"
href="#"
>
Read article
<ArrowRightIcon className="size-3.5 transition-transform group-hover:translate-x-0.5" />
</Link>
</div>
</motion.article>
</AnimatePresence>
<div className="mt-4 flex justify-center gap-1.5">
{posts.map((post, index) => (
<button
aria-label={`Feature ${post.title}`}
className={cn(
"h-1.5 rounded-full transition-all",
index === active ? "w-6 bg-foreground" : "w-1.5 bg-foreground/25",
)}
key={post.title}
onClick={() => setActive(index)}
type="button"
/>
))}
</div>
</div>
</SectionShell>
);
}
















