Editorial blog spotlight — thumbnail rail, spring crossfade feature panel, auto-advance on pause-hover.
@nusaiba
registry/blocks/blog/2/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 { DecorIcon } from "@/components/decor-icon";
import { GridPattern } from "@/components/ui/grid-pattern";
import { cn } from "@/lib/utils";
import { SectionShell, ContentRail } from "@/components/layout-contract";
const spring = { type: "spring" as const, stiffness: 380, damping: 28 };
const posts = [
{
title: "How we cut release cycles in half",
excerpt:
"A practical playbook for smaller teams shipping on tight deadlines — from scoping rituals to async review loops that actually stick.",
category: "Product",
date: "Mar 12, 2026",
readTime: "6 min read",
image: "/images/visuals/hyperspeed.png",
alt: "Neon light trails flowing through a dark space",
},
{
title: "Designing onboarding that converts",
excerpt:
"Five interaction patterns we use to activate users in the first session, without overwhelming the UI or the team maintaining it.",
category: "Design",
date: "Mar 4, 2026",
readTime: "5 min read",
image: "/images/visuals/orb.png",
alt: "Glowing cyan and violet orbital ring",
},
{
title: "What SOC 2 taught our engineering team",
excerpt:
"Security controls that improved how we build software — not just checkbox compliance, but clearer ownership and safer defaults.",
category: "Engineering",
date: "Feb 22, 2026",
readTime: "8 min read",
image: "/images/visuals/color-bends.png",
alt: "Purple and magenta color bends with film grain",
},
{
title: "Motion tokens for marketing pages",
excerpt:
"How we tune spring easing, stagger delays, and whileInView thresholds so sections feel intentional — not like a generic animation pack.",
category: "Craft",
date: "Feb 8, 2026",
readTime: "7 min read",
image: "/images/visuals/ripple-grid.png",
alt: "Distorted white grid glowing on black",
},
];
export function BlogSpotlightSection() {
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);
}, 5600);
return () => clearInterval(timer);
}, [paused]);
return (
<SectionShell spacingMode="section" className="relative isolate overflow-hidden">
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10">
<div className="absolute top-1/2 left-1/2 h-[420px] w-[640px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/[0.06] blur-3xl" />
<GridPattern
className="absolute inset-0 mask-[radial-gradient(ellipse_at_center,white,transparent_72%)] stroke-foreground/[0.05]"
height={48}
width={48}
/>
</div>
<ContentRail maxWidth="max-w-5xl" className="space-y-12">
<motion.div
className="mx-auto max-w-2xl text-center"
initial={{ opacity: 0, y: 16 }}
transition={spring}
viewport={{ once: true }}
whileInView={{ opacity: 1, y: 0 }}
>
<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">
Stories from teams shipping in public
</h2>
<p className="mt-4 text-muted-foreground text-sm md:text-base">
Product craft, motion notes, and lessons from building sections that feel
alive — not another generic blog grid.
</p>
</motion.div>
<motion.div
className="relative border border-border bg-border p-px"
initial={{ opacity: 0, y: 20 }}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
transition={{ ...spring, delay: 0.08 }}
viewport={{ once: true }}
whileInView={{ opacity: 1, y: 0 }}
>
<DecorIcon position="top-left" />
<DecorIcon position="top-right" />
<DecorIcon position="bottom-left" />
<DecorIcon position="bottom-right" />
<div className="grid bg-background lg:grid-cols-[minmax(0,12.5rem)_minmax(0,1fr)]">
<div className="grid grid-cols-2 gap-2 border-b border-border p-4 sm:grid-cols-4 lg:grid-cols-1 lg:gap-2.5 lg:border-r lg:border-b-0 lg:p-5">
{posts.map((post, index) => {
const isActive = index === active;
return (
<button
aria-label={`Show article: ${post.title}`}
aria-pressed={isActive}
className={cn(
"group rounded-[var(--radius-md)] border p-2 text-left transition-all",
isActive
? "border-foreground/20 bg-muted/50 shadow-sm"
: "border-transparent opacity-60 hover:border-border hover:opacity-100",
)}
key={post.title}
onClick={() => setActive(index)}
type="button"
>
<div
className={cn(
"relative aspect-[4/3] overflow-hidden rounded-[var(--radius-sm)] border border-border/80 bg-muted/30",
isActive && "ring-1 ring-foreground/10",
)}
>
<Image
alt={post.alt}
className="object-cover transition-transform duration-500 group-hover:scale-[1.03]"
fill
sizes="120px"
src={post.image}
/>
</div>
<p className="mt-2 line-clamp-1 font-medium text-[11px]">
{post.category}
</p>
<p className="mt-0.5 line-clamp-2 text-[10px] text-muted-foreground leading-snug">
{post.title}
</p>
</button>
);
})}
</div>
<div className="relative min-h-[24rem] p-6 md:p-8 lg:min-h-[26rem]">
<AnimatePresence mode="wait">
<motion.article
animate={{ opacity: 1, y: 0 }}
className="flex h-full flex-col"
exit={{ opacity: 0, y: -12 }}
initial={{ opacity: 0, y: 16 }}
key={activePost.title}
transition={spring}
>
<div className="relative aspect-[16/10] overflow-hidden rounded-[var(--radius-md)] border border-border bg-muted/20">
<motion.div
animate={{ scale: 1 }}
className="absolute inset-0"
initial={{ scale: 1.04 }}
transition={{ duration: 0.7, ease: "easeOut" }}
>
<Image
alt={activePost.alt}
className="object-cover"
fill
priority
sizes="(max-width: 1024px) 100vw, 640px"
src={activePost.image}
/>
</motion.div>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_30%_0%,oklch(1_0_0/0.08),transparent_55%)]" />
</div>
<div className="mt-5 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground text-xs">
<span className="font-medium text-foreground">
{activePost.category}
</span>
<span aria-hidden>·</span>
<span>{activePost.date}</span>
<span aria-hidden>·</span>
<span className="inline-flex items-center gap-1">
<ClockIcon className="size-3" />
{activePost.readTime}
</span>
</div>
<h3 className="mt-3 text-balance font-medium text-xl tracking-tight md:text-2xl">
<Link className="hover:underline" href="#">
{activePost.title}
</Link>
</h3>
<p className="mt-3 max-w-2xl text-muted-foreground text-sm leading-relaxed md:text-base">
{activePost.excerpt}
</p>
<Link
className="group mt-auto inline-flex items-center gap-1.5 pt-6 font-medium text-sm"
href="#"
>
Read article
<ArrowRightIcon className="size-3.5 transition-transform group-hover:translate-x-0.5" />
</Link>
</motion.article>
</AnimatePresence>
</div>
</div>
</motion.div>
</ContentRail>
</SectionShell>
);
}
















