# Media editorial

> Browses a complete visual feature through chapters, focus galleries, and scroll-led storytelling.

A shadcn/ui block from the [@motion-lexicon](https://motion-lexicon.pages.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/motion-lexicon/media-editorial).

- Page, with a live preview: https://shadcn.exchange/r/motion-lexicon/media-editorial
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/motion-lexicon/media-editorial-light-645be436c479b567.png
- Registry item: https://motion-lexicon.pages.dev/r/media-editorial.json
- Categories: page-block
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://motion-lexicon.pages.dev/r/media-editorial.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: `https://motion-lexicon.pages.dev/r/cinematic-hero.json`, `https://motion-lexicon.pages.dev/r/focus-gallery.json`, `https://motion-lexicon.pages.dev/r/kinetic-heading.json`, `https://motion-lexicon.pages.dev/r/scroll-story.json`

## Notes from the registry

https://motion-lexicon.pages.dev/en/blocks/media-editorial/

## Files

### `src/registry/blocks/media-editorial.tsx`

```tsx
"use client";

import { motion, useReducedMotion } from "motion/react";
import { useState } from "react";
import { CinematicHero } from "@/components/motion-lexicon/cinematic-hero";
import {
  FocusGallery,
  type FocusGalleryItem,
} from "@/components/motion-lexicon/focus-gallery";
import { KineticHeading } from "@/components/motion-lexicon/kinetic-heading";
import { ScrollStory } from "@/components/motion-lexicon/scroll-story";

type Locale = "zh" | "en";
export type MediaEditorialBlockProps = { locale?: Locale; className?: string };

const art = (tone: string, accent: string) => (
  <div aria-hidden className={`relative size-full overflow-hidden ${tone}`}>
    <span
      className={`absolute -left-[8%] bottom-[-34%] size-[78%] rounded-full border-[18px] ${accent}`}
    />
    <span className="absolute right-[18%] top-[15%] h-[68%] w-[9%] rounded-full bg-[#e06d4a]" />
    <span className="absolute inset-x-0 bottom-[19%] h-px bg-white/70" />
  </div>
);

export function MediaEditorialBlock({
  locale = "en",
  className = "",
}: MediaEditorialBlockProps) {
  const reduced = useReducedMotion() === true;
  const [joined, setJoined] = useState(false);
  const copy = locale === "zh" ? zh : en;
  const gallery: readonly FocusGalleryItem[] = [
    {
      id: "harbour",
      title: copy.harbour,
      caption: copy.harbourCopy,
      meta: "NORTH / 01",
      art: art("bg-[#18475e]", "border-[#efbd6c]"),
    },
    {
      id: "orchard",
      title: copy.orchard,
      caption: copy.orchardCopy,
      meta: "NORTH / 02",
      art: art("bg-[#b85a43]", "border-[#f2c06c]"),
    },
    {
      id: "signal",
      title: copy.signal,
      caption: copy.signalCopy,
      meta: "NORTH / 03",
      art: art("bg-[#29204c]", "border-[#b69aff]"),
    },
  ];
  return (
    <motion.section
      data-page-block="media-editorial"
      initial={reduced ? false : { opacity: 0, y: 8 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: reduced ? 0 : 0.28 }}
      className={`w-full overflow-hidden rounded-[18px] border border-[#4b3526]/15 bg-[#f4ead9] text-[#2e251d] ${className}`}
    >
      <header className="flex min-h-16 items-center justify-between gap-4 border-b border-[#4b3526]/12 px-5 sm:px-8">
        <a
          href="#editorial-home"
          className="flex min-h-11 items-center gap-2 font-serif text-[22px] tracking-[-.06em] outline-none focus-visible:ring-2 focus-visible:ring-[#4568ff]"
        >
          Northline{" "}
          <span className="font-mono text-[9px] tracking-[.16em]">JOURNAL</span>
        </a>
        <nav
          aria-label={copy.navigation}
          className="hidden gap-4 text-[11px] font-medium text-[#745f4b] md:flex"
        >
          <a href="#editorial-stories">{copy.stories}</a>
          <a href="#editorial-gallery">{copy.gallery}</a>
          <a href="#editorial-letter">{copy.letter}</a>
        </nav>
        <button
          type="button"
          onClick={() => setJoined(true)}
          className="min-h-11 rounded-full bg-[#2f251b] px-4 text-[11px] font-semibold text-[#fff2de] outline-none focus-visible:ring-2 focus-visible:ring-[#4568ff]"
        >
          {joined ? copy.joined : copy.subscribe}
        </button>
      </header>
      <div id="editorial-home" className="p-4 sm:p-6 lg:p-8">
        <CinematicHero
          eyebrow={copy.issue}
          title={copy.title}
          description={copy.intro}
          actionLabel={copy.read}
          art={art(
            "bg-[linear-gradient(126deg,#17475f_0_44%,#d96847_44%_50%,#395b50_50%)]",
            "border-[#f0bd68]",
          )}
        />
        <section
          id="editorial-stories"
          className="mt-8 grid gap-6 lg:grid-cols-[minmax(0,1.18fr)_minmax(260px,.82fr)]"
        >
          <div>
            <span className="font-mono text-[9px] tracking-[.16em] text-[#80674e]">
              {copy.featured}
            </span>
            <KineticHeading
              text={copy.headline}
              label={copy.featured}
              detail={copy.body}
              className="mt-3"
            />
            <div className="mt-6 flex items-center gap-3 border-y border-[#4b3526]/12 py-4">
              <span className="grid size-9 place-items-center rounded-full bg-[#31546a] font-serif text-white">
                N
              </span>
              <span>
                <strong className="block text-[11px]">Mina Alvarez</strong>
                <span className="text-[9px] text-[#80674e]">{copy.byline}</span>
              </span>
            </div>
          </div>
          <ScrollStory
            label={copy.storyLabel}
            height={360}
            chapters={[
              {
                id: "arrive",
                eyebrow: "01",
                title: copy.arrive,
                copy: copy.arriveCopy,
                scene: art("bg-[#20475b]", "border-[#f0bd68]"),
              },
              {
                id: "listen",
                eyebrow: "02",
                title: copy.listen,
                copy: copy.listenCopy,
                scene: art("bg-[#703d3f]", "border-[#f2c06c]"),
              },
              {
                id: "return",
                eyebrow: "03",
                title: copy.return,
                copy: copy.returnCopy,
                scene: art("bg-[#2d2859]", "border-[#bba8ff]"),
              },
            ]}
          />
        </section>
        <section id="editorial-gallery" className="mt-10">
          <FocusGallery items={gallery} label={copy.galleryLabel} />
        </section>
        <section
          id="editorial-letter"
          className="mt-8 grid gap-4 border-t border-[#4b3526]/12 pt-8 sm:grid-cols-[1fr_auto] sm:items-end"
        >
          <div>
            <span className="font-mono text-[9px] tracking-[.16em] text-[#80674e]">
              {copy.letter}
            </span>
            <h2 className="mt-2 max-w-[18ch] font-serif text-[30px] leading-[.9] tracking-[-.06em]">
              {copy.letterTitle}
            </h2>
          </div>
          <button
            type="button"
            onClick={() => setJoined(true)}
            className="min-h-11 rounded-full border border-[#4b3526]/20 bg-[#fff6e9] px-4 text-[11px] font-semibold outline-none focus-visible:ring-2 focus-visible:ring-[#4568ff]"
          >
            {joined ? copy.joined : copy.subscribe}
          </button>
        </section>
      </div>
    </motion.section>
  );
}
const en = {
  navigation: "Journal navigation",
  stories: "Stories",
  gallery: "Gallery",
  letter: "Letter",
  subscribe: "Subscribe",
  joined: "Subscribed",
  issue: "NORTHLINE / AUTUMN 2026",
  title: "Listen between tides",
  intro:
    "A visual journal of harbour light, late ferries and the frequencies that remain after midnight.",
  read: "Read the issue",
  featured: "FEATURED STORY",
  headline: "A harbour keeps its own weather.",
  body: "For three November evenings, we followed the last crossing from the old breakwater to the edge of the city. The route is short; the light never repeats itself.",
  byline: "Words and images · 8 min read",
  storyLabel: "Harbour story",
  arrive: "Arrive",
  arriveCopy: "The ferry leaves before the street lamps settle.",
  listen: "Listen",
  listenCopy: "Wind makes a radio of every railing.",
  return: "Return",
  returnCopy: "The same coast becomes a different page.",
  galleryLabel: "Field studies",
  harbour: "Harbour after rain",
  harbourCopy: "Wet cobalt, rust and reflected traffic.",
  orchard: "Orchard table",
  orchardCopy: "Late fruit, linen and the last usable daylight.",
  signal: "Night score",
  signalCopy: "An ultraviolet pulse against a sodium-orange line.",
  letterTitle: "One new field note, sent when it is ready.",
};
const zh = {
  navigation: "刊物导航",
  stories: "故事",
  gallery: "画廊",
  letter: "来信",
  subscribe: "订阅",
  joined: "已订阅",
  issue: "北岸 / 2026 秋季册",
  title: "在潮汐之间聆听",
  intro: "一本关于港湾光线、末班渡轮与午夜后仍在回响的频率的影像日记。",
  read: "阅读本期",
  featured: "本期特写",
  headline: "港湾拥有自己的天气。",
  body: "连续三个十一月夜晚，我们跟随最后一班渡轮，从旧防波堤抵达城市边缘。航线很短，光线从未重复。",
  byline: "文字与影像 · 阅读约 8 分钟",
  storyLabel: "港湾故事",
  arrive: "抵达",
  arriveCopy: "街灯尚未安定，渡轮已经离岸。",
  listen: "聆听",
  listenCopy: "每一段栏杆都被风吹成电台。",
  return: "返回",
  returnCopy: "同一片海岸成为另一张页面。",
  galleryLabel: "现场研究",
  harbour: "雨后的港湾",
  harbourCopy: "湿润钴蓝、锈色与街灯倒影。",
  orchard: "果园餐桌",
  orchardCopy: "晚熟果实、亚麻与最后的可用日光。",
  signal: "夜间乐谱",
  signalCopy: "紫外脉冲停留在钠灯橙线旁。",
  letterTitle: "一则新的现场笔记，在它准备好时抵达。",
};
```
