# Hero 12

> Four-tile gallery wall motion showcase with hover-lift and registry-native captions.

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

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

## Install

```bash
npx shadcn@latest add https://nusaiba.dev/r/base-nova/hero-12.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`, `badge`, `button`

## Files

### `registry/blocks/hero/hero-data.ts`

```ts
export const heroSite = {
	name: "Nusaiba",
	namespace: "@nusaiba",
	domain: "nusaiba.dev",
} as const;

export const heroAnnouncement = {
	label: "Open registry",
	detail: "Motion-first shadcn blocks",
} as const;

export const heroCopy = {
	headline: "Sections built for motion.",
	subhead:
		"Preview every block live, install with the CLI, and keep source in your repo — no runtime lock-in.",
	primaryCta: { label: "Browse blocks", href: "/blocks" },
	secondaryCta: { label: "View docs", href: "/docs" },
} as const;

export const heroPosterCopy = {
	headline: ["Compose", "landing", "pages", "with", "intent."],
	subhead:
		"Editorial sections for developers who care about rhythm, type, and installable motion — not another SaaS template.",
} as const;

export const heroArchitectureCopy = {
	eyebrow: "Registry",
	headline: "Invisible grid. Visible craft.",
	subhead:
		"Each block ships as files you own. Wire copy, tokens, and motion to the product story you are telling.",
} as const;

export const heroMagazineCopy = {
	issue: "Vol. 03",
	headline: "Motion is the interface.",
	pullQuote: "Less decoration. More composition.",
	subhead:
		"Heroes, pricing, FAQs, footers — staged for scroll-reveal, stagger, and purposeful negative space.",
	trustLabel: "Installs beside",
} as const;

export const heroMagazineTrustBrands = [
	{ name: "Next.js", domain: "nextjs.org" },
	{ name: "Vercel", domain: "vercel.com" },
	{ name: "Linear", domain: "linear.app" },
	{ name: "Notion", domain: "notion.so" },
] as const;

export const heroFilmCopy = {
	headline: "A library that reads like a magazine.",
	subhead:
		"Scan live previews, copy a prompt, or pull a section with one command. Every layout is a distinct archetype.",
} as const;

export const heroPerspectiveCopy = {
	headline: "Depth before decoration.",
	subhead:
		"Layers imply movement before animation runs — perspective, overlap, and reveal composition baked into the layout.",
} as const;

export const heroGalleryCopy = {
	headline: "Ten families. One install path.",
	subhead:
		"From heroes to footers, each category explores a different visual narrative for landing pages.",
} as const;

export const heroDiagonalCopy = {
	headline: "Cut against the grid.",
	subhead:
		"Diagonal rhythm and masked stages — editorial tension without gradients, blobs, or stock dashboards.",
} as const;

export const heroFramedCopy = {
	headline: "Framed for the fold.",
	subhead:
		"Museum-grade spacing, decor corners, and a motion stage that feels exhibition-ready on first paint.",
} as const;

export const heroShowcaseBlocks = [
	{ id: "hero", label: "Hero", pattern: "scroll-reveal" },
	{ id: "pricing", label: "Pricing", pattern: "focus-shift" },
	{ id: "faqs", label: "FAQs", pattern: "blur-focus" },
	{ id: "cta", label: "CTA", pattern: "expand-grid" },
	{ id: "footer", label: "Footer", pattern: "fade-mask" },
] as const;

export const heroGalleryTiles = [
	{ id: "motion", title: "Motion patterns", caption: "Linked to live blocks" },
	{ id: "cli", title: "shadcn CLI", caption: "npx shadcn@latest add" },
	{ id: "prompt", title: "Copy prompt", caption: "Registry-native handoff" },
	{ id: "pro", title: "Pro license", caption: "Commercial client work" },
] as const;
```

### `registry/blocks/hero/hero-primitives.tsx`

```tsx
"use client";

import Link from "next/link";
import { useRef, useState } from "react";
import { ArrowRightIcon } from "lucide-react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { Badge } from "@/components/ui/badge";
import { buttonVariants } from "@/components/ui/button";
import { BrandFonts, BrandWordmark } from "@/lib/brand-typography";
import { motionStagger, motionViewport } from "@/lib/motion-tokens";
import { cn } from "@/lib/utils";
import { heroAnnouncement, heroCopy } from "./hero-data";

const springPress = "active:scale-[0.96] transition-transform";

const logoIconClass =
	"size-5 shrink-0 object-contain outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/10";

function getLogoDevUrl(domain: string) {
	const token = process.env.NEXT_PUBLIC_LOGO_DEV_PUBLISHABLE_KEY;
	if (!token) return null;
	const params = new URLSearchParams({
		token,
		size: "80",
		format: "png",
		retina: "true",
		fallback: "monogram",
	});
	return `https://img.logo.dev/${domain}?${params}`;
}

export function HeroAnnouncement({
	className,
	detail = heroAnnouncement.detail,
	label = heroAnnouncement.label,
}: {
	className?: string;
	detail?: string;
	label?: string;
}) {
	return (
		<div className={cn("flex flex-wrap items-center gap-2", className)}>
			<Badge variant="outline">{label}</Badge>
			<span className="text-muted-foreground text-xs tracking-wide">{detail}</span>
		</div>
	);
}

export function HeroCtaRow({
	className,
	primary = heroCopy.primaryCta,
	secondary = heroCopy.secondaryCta,
}: {
	className?: string;
	primary?: { label: string; href: string };
	secondary?: { label: string; href: string };
}) {
	return (
		<div className={cn("flex flex-col gap-3 sm:flex-row sm:items-center", className)}>
			<Link className={cn(buttonVariants(), springPress)} href={primary.href}>
				{primary.label}
				<ArrowRightIcon data-icon="inline-end" />
			</Link>
			<Link
				className={cn(buttonVariants({ variant: "outline" }), springPress)}
				href={secondary.href}
			>
				{secondary.label}
			</Link>
		</div>
	);
}

export function MotionShowcaseFrame({
	children,
	className,
	label,
}: {
	children: React.ReactNode;
	className?: string;
	label?: string;
}) {
	return (
		<div
			className={cn(
				"relative overflow-hidden border border-border bg-background shadow-[0_1px_0_rgba(0,0,0,0.03)] dark:shadow-[0_1px_0_rgba(255,255,255,0.04)]",
				className,
			)}
		>
			{label ? (
				<p className="absolute top-3 left-3 z-10 font-mono text-[10px] text-muted-foreground uppercase tracking-[0.2em]">
					{label}
				</p>
			) : null}
			{children}
		</div>
	);
}

export function BlockStripChip({
	label,
	pattern,
}: {
	label: string;
	pattern: string;
}) {
	return (
		<div className="flex w-[11rem] shrink-0 flex-col justify-between border border-border bg-card/60 p-4">
			<p className="font-medium text-sm tracking-tight">{label}</p>
			<p className="mt-8 font-mono text-[10px] text-muted-foreground uppercase tracking-[0.18em]">
				{pattern}
			</p>
		</div>
	);
}

export function SwissRule({ className }: { className?: string }) {
	return <div aria-hidden className={cn("h-px w-full bg-border", className)} />;
}

export function HeroInlineLogoStrip({
	brands,
	className,
	label,
}: {
	brands: readonly { name: string; domain: string }[];
	className?: string;
	label: string;
}) {
	const reduceMotion = useReducedMotion();
	const listRef = useRef<HTMLUListElement>(null);
	const inView = useInView(listRef, motionViewport);
	const [focused, setFocused] = useState<string | null>(null);

	const itemVariants = reduceMotion
		? { hidden: { opacity: 1 }, visible: { opacity: 1 } }
		: {
				hidden: { opacity: 0, filter: "blur(4px)", y: 6 },
				visible: { opacity: 1, filter: "blur(0px)", y: 0, transition: { type: "spring" as const, duration: 0.35, bounce: 0 } },
			};

	return (
		<div className={cn("max-w-md", className)}>
			<BrandFonts id="hero-inline-logo-strip-fonts" />
			<p className="font-mono text-[10px] text-muted-foreground uppercase tracking-[0.2em]">
				{label}
			</p>
			<motion.ul
				animate={inView ? "visible" : "hidden"}
				className="mt-4 flex flex-wrap items-center gap-x-5 gap-y-3 sm:gap-x-6"
				initial="hidden"
				onMouseLeave={() => setFocused(null)}
				ref={listRef}
				variants={{
					hidden: {},
					visible: {
						transition: { staggerChildren: motionStagger.item, delayChildren: 0.04 },
					},
				}}
			>
				{brands.map((brand) => {
					const src = getLogoDevUrl(brand.domain);
					const isFocused = focused === brand.domain;
					const dimmed = focused !== null && !isFocused;

					return (
						<motion.li key={brand.domain} variants={itemVariants}>
							<motion.button
								animate={
									reduceMotion
										? undefined
										: {
												opacity: dimmed ? 0.42 : 1,
												filter: dimmed ? "blur(3px)" : "blur(0px)",
												scale: isFocused ? 1.04 : 1,
											}
								}
								aria-label={brand.name}
								className="flex h-6 items-center gap-2 rounded-sm outline-none transition-colors active:scale-[0.96]"
								onBlur={() => setFocused((current) => (current === brand.domain ? null : current))}
								onFocus={() => setFocused(brand.domain)}
								onMouseEnter={() => setFocused(brand.domain)}
								transition={{ type: "spring", duration: 0.3, bounce: 0 }}
								type="button"
							>
								{src ? (
									<img
										alt=""
										aria-hidden
										className={logoIconClass}
										height={20}
										loading="lazy"
										referrerPolicy="origin"
										src={src}
									/>
								) : null}
								<BrandWordmark
									className={cn(
										"text-[0.75rem] leading-none sm:text-[0.8125rem]",
										isFocused ? "text-foreground" : "text-foreground/75",
										!src && "opacity-80",
									)}
									domain={brand.domain}
									name={brand.name}
								/>
							</motion.button>
						</motion.li>
					);
				})}
			</motion.ul>
		</div>
	);
}
```

### `registry/blocks/hero/12/hero-section.tsx`

```tsx
"use client";

import Link from "next/link";
import { useRef } from "react";
import { ArrowUpRightIcon } from "lucide-react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { ContentRail, SectionShell } from "@/components/layout-contract";
import { motionStagger, motionViewport } from "@/lib/motion-tokens";
import { cn } from "@/lib/utils";
import { heroGalleryCopy, heroGalleryTiles } from "../hero-data";
import { HeroAnnouncement, HeroCtaRow } from "../hero-primitives";

const spring = { type: "spring" as const, duration: 0.35, bounce: 0 };

export function HeroGalleryWallSection() {
	const reduceMotion = useReducedMotion();
	const ref = useRef<HTMLElement>(null);
	const inView = useInView(ref, motionViewport);

	const itemVariants = reduceMotion
		? { hidden: { opacity: 1 }, visible: { opacity: 1 } }
		: {
				hidden: { opacity: 0, filter: "blur(4px)", y: 12 },
				visible: { opacity: 1, filter: "blur(0px)", y: 0, transition: spring },
			};

	return (
		<SectionShell spacingMode="section" ref={ref}>
			<motion.div
				animate={inView ? "visible" : "hidden"}
				className="w-full"
				initial="hidden"
				variants={{
					hidden: {},
					visible: { transition: { staggerChildren: motionStagger.item, delayChildren: 0.05 } },
				}}
			>
				<ContentRail>
				<motion.div variants={itemVariants}>
					<HeroAnnouncement />
				</motion.div>
				<motion.h1
					className="mt-8 max-w-[14ch] text-balance font-medium text-4xl tracking-[-0.04em] md:text-5xl"
					variants={itemVariants}
				>
					{heroGalleryCopy.headline}
				</motion.h1>
				<motion.p
					className="mt-4 max-w-lg text-pretty text-muted-foreground text-sm md:text-base"
					variants={itemVariants}
				>
					{heroGalleryCopy.subhead}
				</motion.p>

				<div className="mt-12 grid gap-3 sm:grid-cols-2">
					{heroGalleryTiles.map((tile) => (
						<motion.div key={tile.id} variants={itemVariants}>
							<Link
								className={cn(
									"group flex h-full min-h-[9rem] flex-col justify-between border border-border bg-card/30 p-6",
									"transition-[transform,border-color,box-shadow] hover:-translate-y-0.5 hover:border-foreground/20 hover:shadow-md active:scale-[0.99]",
								)}
								href="/blocks"
							>
								<div>
									<p className="font-medium text-lg tracking-tight">{tile.title}</p>
									<p className="mt-2 text-muted-foreground text-sm">{tile.caption}</p>
								</div>
								<ArrowUpRightIcon className="size-4 text-muted-foreground transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
							</Link>
						</motion.div>
					))}
				</div>

				<motion.div className="mt-10" variants={itemVariants}>
					<HeroCtaRow />
				</motion.div>
				</ContentRail>
			</motion.div>
		</SectionShell>
	);
}
```
