the lines under an open title: episode, time left, facts, its story
npx shadcn add @ag/poster-captionAlready installed? Preview an update
npx shadcn add @ag/poster-caption --dry-runYour installed source stays unchanged until you update it. How updates work
Latest changes · 2026-11-05
- New: the lines under an open title,
<PosterCaption eyebrow headline facts synopsis />: an episode and the time left for a title started, its facts and its story for one not, the synopsis always holding the room of three lines. Its type is a share of its container's width.
shelf media
where it was left: the episode, then the time left
S3, E2 · Witness
49 min left
its facts, then its story, always the room of three lines
SeriesDrama202410 episodes12
In the countryside of the sixties, a young woman marries to save her family's farm, and finds the house she moves into full of locked rooms nobody will open, and a husband who keeps the keys.
the demo above, verbatim
import { Sample } from "@/app/samples"
import { TitleFacts } from "@/registry/base-nova/ui/media-spec"
import { PosterCaption } from "@/registry/base-nova/ui/poster-caption"
export default function Demo() {
return (
<div className="space-y-12">
<Sample
name="started"
glance
title="a title started"
description="where it was left: the episode, then the time left"
>
{/* Its type is a share of its container's width: a page's here. */}
<div className="@container w-[60rem] max-w-full">
<PosterCaption eyebrow="S3, E2 · Witness" headline="49 min left" />
</div>
</Sample>
<Sample
name="new"
title="a title not started"
description="its facts, then its story, always the room of three lines"
>
<div className="@container w-[60rem] max-w-full">
<PosterCaption
className="w-1/2"
facts={
<TitleFacts
facts={["Series", "Drama", "2024", "10 episodes"]}
age={12}
/>
}
synopsis="In the countryside of the sixties, a young woman marries to save her family's farm, and finds the house she moves into full of locked rooms nobody will open, and a husband who keeps the keys."
/>
</div>
</Sample>
</div>
)
}the source, every file an install writes
import type * as React from "react"
import { cn } from "@/lib/utils"
// WHAT IS UNDER AN OPEN CARD: the lines a browse row shows beneath the title it is on.
// Up to four, each optional, in this order: a quiet line naming it ("S3, E2 · Witness",
// or the title itself), a lead line ("49 min left"), the facts (media-spec's
// `TitleFacts`: kind, genre, year, length, age), and a synopsis that always holds the
// room of three lines, so the shelf under it stands where it stands whatever the
// synopsis says. In the text's colour at the registry's tiers.
//
// Its type and lines are shares of the width of the container it is in (a shelf, as
// wide as its page), as measured on a TV's browse row: a line every 21 px of 1087 for
// the lead lines (cap heights 10 and 11), 19.5 for the synopsis (cap 11.5), 7 between.
const LINE =
"h-[1.93cqw] truncate leading-[1.93cqw] text-[1.38cqw] text-foreground/93"
export function PosterCaption({
eyebrow,
headline,
facts,
synopsis,
className,
}: {
eyebrow?: React.ReactNode
headline?: React.ReactNode
facts?: React.ReactNode
synopsis?: React.ReactNode
className?: string
}) {
const leads = Boolean(eyebrow || headline || facts)
return (
<div
data-slot="poster-caption"
className={cn("flex flex-col text-foreground", className)}
>
{eyebrow && (
<p
data-slot="poster-caption-eyebrow"
className={cn(LINE, "font-medium text-[1.26cqw] text-foreground/61")}
>
{eyebrow}
</p>
)}
{headline && (
<p
data-slot="poster-caption-headline"
className={cn(LINE, "font-medium")}
>
{headline}
</p>
)}
{facts && (
<div
data-slot="poster-caption-facts"
className={cn(LINE, "flex items-center font-semibold")}
>
{facts}
</div>
)}
{synopsis && (
<p
data-slot="poster-caption-synopsis"
className={cn(
"line-clamp-3 min-h-[5.382cqw] text-[1.45cqw] text-foreground/86 leading-[1.794cqw]",
leads && "mt-[0.65cqw]",
)}
>
{synopsis}
</p>
)}
</div>
)
}