A long-form reader with a scoped scroll-progress bar, drop-cap opening paragraph, and pull-quote styling.
npx shadcn@latest add @paragon/article-reader"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface ArticleReaderProps extends React.ComponentProps<"article"> {
category?: string;
title?: string;
byline?: string;
dateline?: string;
/** Reading-time label, e.g. "6 min read". */
readingTime?: string;
/** Body: paragraphs of text, optionally interleaved with pull quotes. */
children?: React.ReactNode;
}
/**
* A long-form NYT-style reader: a scoped scroll-progress bar pinned to the top
* of the article, a drop-cap first paragraph, and pull-quote styling. The
* progress bar tracks THIS article's own scroll within a bounded, scrollable
* container (not the window), so it works inside a docs preview. It updates via
* a rAF-throttled scroll listener and is decorative/aria-hidden.
*/
export function ArticleReader({
category = "Business",
title = "The Quiet Restructuring Reshaping Corporate Finance",
byline = "By Dana Whitfield",
dateline = "March 14, 2026",
readingTime = "6 min read",
className,
children,
...props
}: ArticleReaderProps) {
const scrollRef = React.useRef<HTMLDivElement>(null);
const barRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
const el = scrollRef.current;
const bar = barRef.current;
if (!el || !bar) return;
let frame = 0;
const update = () => {
frame = 0;
const max = el.scrollHeight - el.clientHeight;
const pct = max > 0 ? Math.min(1, el.scrollTop / max) : 0;
bar.style.transform = `scaleX(${pct})`;
};
const onScroll = () => {
if (frame) return;
frame = requestAnimationFrame(update);
};
update();
el.addEventListener("scroll", onScroll, { passive: true });
return () => {
el.removeEventListener("scroll", onScroll);
if (frame) cancelAnimationFrame(frame);
};
}, []);
const body = children ?? <DefaultBody />;
return (
<div
className={cn(
"relative w-full max-w-xl overflow-hidden rounded-xl bg-card text-card-foreground shadow-border",
className,
)}
>
<div
aria-hidden
className="absolute inset-x-0 top-0 z-10 h-0.5 bg-border/60"
>
<div
ref={barRef}
className="h-full origin-left bg-foreground"
style={{ transform: "scaleX(0)" }}
/>
</div>
<div
ref={scrollRef}
className="max-h-[26rem] overflow-y-auto overscroll-contain"
>
<article
data-slot="article-reader"
className="px-6 py-6 [&_p+p]:mt-4"
{...props}
>
<header className="mb-5">
<span className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
{category}
</span>
<h1 className="mt-2 text-2xl font-semibold leading-tight tracking-tight text-balance">
{title}
</h1>
<p className="mt-3 flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground">
<span className="font-medium text-foreground">{byline}</span>
<span aria-hidden>·</span>
<span>{dateline}</span>
<span aria-hidden>·</span>
<span className="tabular-nums">{readingTime}</span>
</p>
</header>
<div className="text-[15px] leading-relaxed text-foreground/90 [&_.drop-cap]:first-letter:mr-2 [&_.drop-cap]:first-letter:float-left [&_.drop-cap]:first-letter:text-5xl [&_.drop-cap]:first-letter:leading-[0.8] [&_.drop-cap]:first-letter:font-semibold">
{body}
</div>
</article>
</div>
</div>
);
}
export function PullQuote({ children }: { children: React.ReactNode }) {
return (
<blockquote className="my-5 border-l-2 border-foreground pl-4 text-lg font-medium leading-snug text-balance text-foreground">
{children}
</blockquote>
);
}
function DefaultBody() {
return (
<>
<p className="drop-cap">
For years the maneuver was treated as a footnote — a technical
adjustment buried deep in the appendix of an annual report. Today it
sits at the center of how the largest firms think about their balance
sheets, and the analysts who once ignored it are now paid handsomely to
read between its lines.
</p>
<p>
The shift did not arrive with fanfare. It crept in quarter by quarter,
as treasurers discovered that a modest change in how obligations were
classified could reshape the numbers investors watched most closely.
</p>
<PullQuote>
“Nobody restructures loudly. The whole point is that the market barely
notices until the next earnings call.”
</PullQuote>
<p>
What follows is a look inside that quiet machinery — the incentives, the
oversight gaps, and the handful of decisions that separate a routine
filing from a full restatement.
</p>
<p>
Regulators have taken note. A working group convened last autumn is
weighing new disclosure rules that would drag the practice back into
plain view, forcing firms to explain in ordinary language what today
requires a specialist to decode.
</p>
</>
);
}