# Blog 9

> Infinite article title marquee with crossfading featured story card.

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

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

## Install

```bash
npx shadcn@latest add https://nusaiba.dev/r/base-nova/blog-9.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`, `@nusaiba/gallery-visuals`

## Files

### `registry/blocks/blog/9/blog-section.tsx`

```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>
	);
}
```
