Six releases as live UI mockups rather than screenshots, so they stay sharp and follow your theme. The header shows the version and date, and a caption for the screen under the playhead. Tab to the playhead and use the arrow keys: Shift jumps a release.
@keyframeui
registry/demos/playhead-reel-changelog-demo.tsx
"use client";
import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import { PlayheadReel, type PlayheadReelGroup } from "@/components/ui/playhead-reel";
type Accent = { badge: string; solid: string; soft: string; text: string };
const accents = {
indigo: {
badge: "bg-indigo-500/10 text-indigo-700 dark:text-indigo-300",
solid: "bg-indigo-500",
soft: "bg-indigo-500/15",
text: "text-indigo-500",
},
emerald: {
badge: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
solid: "bg-emerald-500",
soft: "bg-emerald-500/15",
text: "text-emerald-500",
},
amber: {
badge: "bg-amber-500/10 text-amber-700 dark:text-amber-300",
solid: "bg-amber-500",
soft: "bg-amber-500/15",
text: "text-amber-500",
},
rose: {
badge: "bg-rose-500/10 text-rose-700 dark:text-rose-300",
solid: "bg-rose-500",
soft: "bg-rose-500/15",
text: "text-rose-500",
},
sky: {
badge: "bg-sky-500/10 text-sky-700 dark:text-sky-300",
solid: "bg-sky-500",
soft: "bg-sky-500/15",
text: "text-sky-500",
},
violet: {
badge: "bg-violet-500/10 text-violet-700 dark:text-violet-300",
solid: "bg-violet-500",
soft: "bg-violet-500/15",
text: "text-violet-500",
},
} satisfies Record<string, Accent>;
type Release = PlayheadReelGroup & { title: string; date: string; accent: Accent };
const screen = (alt: string, content: React.ReactNode, aspect = 4 / 3) => ({ alt, content, aspect });
const releases: Release[] = [
{
label: "v2.0",
title: "A faster editor",
date: "January 2025",
accent: accents.indigo,
items: [
screen("Pages open instantly, even with thousands of blocks.", <EditorScreen accent={accents.indigo} />),
screen("Type / to insert any block.", <SlashScreen accent={accents.indigo} />),
screen("The editor now works on phones.", <PhoneScreen accent={accents.indigo} />, 3 / 4),
],
},
{
label: "v2.1",
title: "Comments",
date: "March 2025",
accent: accents.emerald,
items: [
screen("Comment on any block and reply in threads.", <ThreadScreen accent={accents.emerald} />),
screen("@mention a teammate to pull them in.", <MentionsScreen accent={accents.emerald} />, 3 / 4),
screen("Resolve a thread when it's done.", <ResolveScreen accent={accents.emerald} />),
],
},
{
label: "v2.2",
title: "Insights",
date: "May 2025",
accent: accents.amber,
items: [
screen("See who's active each week.", <BarsScreen accent={accents.amber} />),
screen("Spot trends over any range.", <TrendScreen accent={accents.amber} />, 3 / 2),
screen("Export any report as CSV, PDF or PNG.", <ExportScreen accent={accents.amber} />, 1),
],
},
{
label: "v2.3",
title: "Boards",
date: "July 2025",
accent: accents.rose,
items: [
screen("Drag work across columns.", <BoardScreen accent={accents.rose} />, 3 / 2),
screen("See every deadline on a calendar.", <CalendarScreen accent={accents.rose} />, 1),
screen("Plan projects on a timeline.", <TimelineScreen accent={accents.rose} />),
],
},
{
label: "v2.4",
title: "Command palette",
date: "September 2025",
accent: accents.sky,
items: [
screen("Search pages, actions and settings from ⌘K.", <PaletteScreen accent={accents.sky} />),
screen("Every action has a shortcut.", <ShortcutsScreen />, 3 / 4),
screen("Dark mode follows your system.", <ThemeScreen accent={accents.sky} />),
],
},
{
label: "v2.5",
title: "Teams",
date: "November 2025",
accent: accents.violet,
items: [
screen("Roles for owners, admins and members.", <MembersScreen accent={accents.violet} />),
screen("Invite a whole team at once.", <InviteScreen accent={accents.violet} />, 1),
screen("An audit log of every change.", <AuditScreen />, 3 / 2),
],
},
];
export function PlayheadReelChangelogDemo() {
const scrollRef = React.useRef<HTMLDivElement>(null);
const [view, setView] = React.useState({ group: 0, item: 0, direction: 1 });
const release = releases[view.group];
const caption = release.items[view.item]?.alt;
return (
<div className="relative w-full overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100">
<div ref={scrollRef} className="relative h-[var(--demo-height,38rem)] overflow-y-auto overscroll-contain">
<PlayheadReel
groups={releases}
containerRef={scrollRef}
stageClassName="h-[var(--demo-height,38rem)]"
itemClassName="rounded-xl ring-1 ring-zinc-900/10 dark:ring-white/10"
thumbHeight={36}
onActiveChange={({ group, item }) =>
setView((prev) => ({
group,
item,
direction: group * 100 + item >= prev.group * 100 + prev.item ? 1 : -1,
}))
}
header={
<header className="px-4 pt-5 pb-4 sm:px-8 sm:pt-7">
<div className="flex items-center justify-between gap-4 font-mono text-[11px] tabular-nums">
<div className="flex items-center gap-2">
<span className="tracking-widest text-zinc-500 uppercase">Changelog</span>
<span
className={cn(
"rounded-full px-2 py-0.5 font-medium transition-colors duration-300",
release.accent.badge,
)}
>
{release.label}
</span>
</div>
<span className="text-zinc-500">{release.date}</span>
</div>
<Roll id={view.group} direction={view.direction} className="mt-2">
<h2 className="truncate text-2xl leading-[1.2] font-semibold tracking-tight sm:text-4xl">
{release.title}
</h2>
</Roll>
<Roll id={view.group * 100 + view.item} direction={view.direction} className="mt-1">
<p className="truncate text-sm leading-6 text-zinc-500">{caption}</p>
</Roll>
</header>
}
/>
</div>
</div>
);
}
/** Rolls its content out and the next one in, up when moving forward and down when going back. */
function Roll({
id,
direction,
className,
children,
}: {
id: number;
direction: number;
className?: string;
children: React.ReactNode;
}) {
const reduceMotion = useReducedMotion();
return (
<div className={cn("relative overflow-hidden", className)}>
<AnimatePresence mode="popLayout" initial={false} custom={direction}>
<motion.div
key={id}
custom={direction}
variants={{
enter: (d: number) => (reduceMotion ? { opacity: 0 } : { y: `${d * 100}%` }),
center: { y: 0, opacity: 1 },
exit: (d: number) => (reduceMotion ? { opacity: 0 } : { y: `${d * -100}%` }),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: reduceMotion ? 0.2 : 0.5, ease: [0.23, 1, 0.32, 1] }}
>
{children}
</motion.div>
</AnimatePresence>
</div>
);
}
/* ── Screens ─────────────────────────────────────────────
* Small product mockups in plain markup. Each fills its item
* at the item's open size, so text stays crisp when open. */
function Window({ title, children, className }: { title?: string; children: React.ReactNode; className?: string }) {
return (
<div className="flex size-full flex-col bg-white text-[10px] leading-tight text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
<div className="flex shrink-0 items-center gap-1 border-b border-zinc-200 px-2.5 py-1.5 dark:border-zinc-800">
<span className="size-1.5 rounded-full bg-zinc-300 dark:bg-zinc-700" />
<span className="size-1.5 rounded-full bg-zinc-300 dark:bg-zinc-700" />
<span className="size-1.5 rounded-full bg-zinc-300 dark:bg-zinc-700" />
{title ? <span className="ml-1.5 truncate text-[9px] text-zinc-400">{title}</span> : null}
</div>
<div className={cn("relative min-h-0 flex-1 overflow-hidden p-3", className)}>{children}</div>
</div>
);
}
function Bar({ width, className }: { width: string; className?: string }) {
return (
<span
className={cn("block h-1.5 shrink-0 rounded-full bg-zinc-200 dark:bg-zinc-800", className)}
style={{ width }}
/>
);
}
function Avatar({ name, className }: { name: string; className: string }) {
return (
<span
className={cn(
"grid size-5 shrink-0 place-items-center rounded-full text-[8px] font-semibold text-white",
className,
)}
>
{name}
</span>
);
}
function Kbd({ children }: { children: React.ReactNode }) {
return (
<kbd className="rounded border border-zinc-200 bg-zinc-50 px-1 font-mono text-[8px] text-zinc-500 dark:border-zinc-700 dark:bg-zinc-800">
{children}
</kbd>
);
}
function EditorScreen({ accent }: { accent: Accent }) {
return (
<Window title="Launch plan">
<div className="flex gap-1">
{["B", "I", "U", "H1", "H2", "“ ”"].map((tool) => (
<span
key={tool}
className="grid h-4 min-w-4 place-items-center rounded bg-zinc-100 px-1 text-[8px] font-semibold text-zinc-500 dark:bg-zinc-800"
>
{tool}
</span>
))}
</div>
<p className="mt-3 text-[14px] font-semibold tracking-tight">Launch plan</p>
<div className="mt-2 space-y-1.5">
<Bar width="94%" />
<Bar width="82%" />
<Bar width="88%" />
</div>
<div className="mt-2 flex items-center gap-1">
<Bar width="46%" className={accent.soft} />
<span className={cn("h-3 w-px", accent.solid)} />
</div>
</Window>
);
}
function SlashScreen({ accent }: { accent: Accent }) {
const blocks = ["Heading", "To-do list", "Image", "Code", "Quote"];
return (
<Window title="Launch plan">
<div className="space-y-1.5">
<Bar width="78%" />
<Bar width="64%" />
</div>
<p className="mt-2 font-mono text-zinc-400">/</p>
<div className="absolute top-12 left-6 w-32 rounded-lg border border-zinc-200 bg-white p-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-800">
{blocks.map((block, i) => (
<div
key={block}
className={cn("flex items-center gap-1.5 rounded px-1.5 py-1", i === 1 && accent.soft)}
>
<span className="size-3 rounded-sm border border-zinc-300 dark:border-zinc-600" />
{block}
</div>
))}
</div>
</Window>
);
}
function PhoneScreen({ accent }: { accent: Accent }) {
return (
<div className="grid size-full place-items-center bg-zinc-100 p-4 dark:bg-zinc-800">
<div className="flex h-full max-h-80 w-full max-w-40 flex-col gap-2 rounded-[1.25rem] border-4 border-zinc-900 bg-white p-3 text-[10px] dark:border-zinc-950 dark:bg-zinc-900">
<span className="mx-auto h-1 w-8 rounded-full bg-zinc-300 dark:bg-zinc-700" />
<p className="mt-1 text-[12px] font-semibold tracking-tight">Launch plan</p>
<Bar width="92%" />
<Bar width="76%" />
<Bar width="84%" />
<div className="mt-auto flex justify-between">
<span className={cn("h-5 w-14 rounded-full", accent.soft)} />
<span className={cn("size-5 rounded-full", accent.solid)} />
</div>
</div>
</div>
);
}
function ThreadScreen({ accent }: { accent: Accent }) {
const comments = [
{ who: "MA", name: "Maya", color: "bg-orange-400", widths: ["80%", "56%"] },
{ who: "JO", name: "Jonas", color: "bg-sky-500", widths: ["68%"] },
];
return (
<Window title="Comments">
<div className="space-y-3">
{comments.map((comment) => (
<div key={comment.who} className="flex gap-2">
<Avatar name={comment.who} className={comment.color} />
<div className="min-w-0 flex-1 space-y-1">
<p className="font-semibold">
{comment.name} <span className="font-normal text-zinc-400">2m</span>
</p>
{comment.widths.map((width) => (
<Bar key={width} width={width} />
))}
</div>
</div>
))}
<div className="flex items-center gap-2 rounded-md border border-zinc-200 px-2 py-1.5 dark:border-zinc-700">
<span className="text-zinc-400">Reply…</span>
<span className={cn("ml-auto rounded px-1.5 py-0.5 font-medium text-white", accent.solid)}>Send</span>
</div>
</div>
</Window>
);
}
function MentionsScreen({ accent }: { accent: Accent }) {
const notes = [
{ who: "MA", color: "bg-orange-400", text: "Maya mentioned you" },
{ who: "JO", color: "bg-sky-500", text: "Jonas replied" },
{ who: "RI", color: "bg-fuchsia-500", text: "Rin mentioned you" },
];
return (
<div className="flex size-full flex-col justify-center gap-2 bg-zinc-100 p-3 text-[10px] dark:bg-zinc-800">
{notes.map((note, i) => (
<div
key={note.who}
className="flex items-center gap-2 rounded-lg bg-white p-2 shadow-sm dark:bg-zinc-900"
style={{ opacity: 1 - i * 0.2 }}
>
<Avatar name={note.who} className={note.color} />
<div className="min-w-0 flex-1">
<p className="truncate font-semibold">{note.text}</p>
<p className="truncate text-zinc-400">in Launch plan</p>
</div>
{i === 0 ? <span className={cn("size-1.5 rounded-full", accent.solid)} /> : null}
</div>
))}
</div>
);
}
function ResolveScreen({ accent }: { accent: Accent }) {
return (
<Window title="Comments">
<div className="rounded-lg border border-zinc-200 p-2.5 dark:border-zinc-700">
<div className="flex items-center gap-2">
<Avatar name="JO" className="bg-sky-500" />
<p className="font-semibold">Jonas</p>
<span className={cn("ml-auto flex items-center gap-1 rounded-full px-1.5 py-0.5 font-medium", accent.badge)}>
<svg viewBox="0 0 12 12" className="size-2.5" aria-hidden>
<path d="M2.5 6.5l2.2 2L9.5 3.5" fill="none" stroke="currentColor" strokeWidth="1.6" />
</svg>
Resolved
</span>
</div>
<div className="mt-2 space-y-1.5 opacity-50">
<Bar width="86%" />
<Bar width="60%" />
</div>
</div>
</Window>
);
}
function BarsScreen({ accent }: { accent: Accent }) {
const heights = [38, 52, 44, 61, 58, 70, 66, 74, 69, 82, 78, 92];
return (
<Window title="Insights">
<div className="flex h-full flex-col">
<p className="text-zinc-400">Weekly active</p>
<div className="flex items-baseline gap-1.5">
<p className="text-[18px] font-semibold tracking-tight tabular-nums">12,480</p>
<span className={cn("rounded-full px-1.5 font-medium", accent.badge)}>+8.2%</span>
</div>
<div className="mt-2 flex min-h-0 flex-1 items-end gap-1">
{heights.map((height, i) => (
<span
key={i}
className={cn("flex-1 rounded-sm", i === heights.length - 1 ? accent.solid : accent.soft)}
style={{ height: `${height}%` }}
/>
))}
</div>
</div>
</Window>
);
}
function TrendScreen({ accent }: { accent: Accent }) {
const points = [62, 58, 64, 55, 49, 52, 44, 38, 42, 33, 28, 30, 22, 18];
const line = points.map((y, i) => `${(i / (points.length - 1)) * 100},${y}`).join(" ");
return (
<Window title="Insights · Last 90 days">
<div className="flex h-full flex-col">
<div className="flex gap-1">
{["7d", "30d", "90d"].map((range) => (
<span
key={range}
className={cn(
"rounded px-1.5 py-0.5 font-medium",
range === "90d" ? accent.badge : "text-zinc-400",
)}
>
{range}
</span>
))}
</div>
<svg viewBox="0 0 100 70" preserveAspectRatio="none" className={cn("mt-2 min-h-0 w-full flex-1", accent.text)} aria-hidden>
<polygon points={`0,70 ${line} 100,70`} fill="currentColor" opacity="0.12" />
<polyline points={line} fill="none" stroke="currentColor" strokeWidth="1.5" vectorEffect="non-scaling-stroke" />
</svg>
</div>
</Window>
);
}
function ExportScreen({ accent }: { accent: Accent }) {
const formats = ["CSV", "PDF", "PNG"];
return (
<div className="grid size-full place-items-center bg-zinc-100 p-4 text-[10px] dark:bg-zinc-800">
<div className="w-full max-w-48 rounded-xl bg-white p-3 shadow-lg dark:bg-zinc-900">
<p className="text-[12px] font-semibold">Export report</p>
<div className="mt-2 space-y-1">
{formats.map((format, i) => (
<div
key={format}
className={cn(
"flex items-center gap-2 rounded-md border px-2 py-1.5",
i === 1 ? "border-transparent " + accent.soft : "border-zinc-200 dark:border-zinc-700",
)}
>
<span
className={cn(
"size-2.5 rounded-full border",
i === 1 ? "border-4 border-current " + accent.text : "border-zinc-300 dark:border-zinc-600",
)}
/>
{format}
</div>
))}
</div>
<div className={cn("mt-2.5 rounded-md py-1.5 text-center font-medium text-white", accent.solid)}>Export</div>
</div>
</div>
);
}
function BoardScreen({ accent }: { accent: Accent }) {
const columns = [
{ name: "To do", cards: 3 },
{ name: "Doing", cards: 2 },
{ name: "Done", cards: 2 },
];
return (
<Window title="Boards · Website">
<div className="grid grid-cols-3 items-start gap-2">
{columns.map((column, c) => (
<div key={column.name} className="space-y-1.5 rounded-lg bg-zinc-100 p-1.5 dark:bg-zinc-800">
<p className="font-semibold text-zinc-500">{column.name}</p>
{Array.from({ length: column.cards }, (_, i) => (
<div key={i} className="space-y-1 rounded-md bg-white p-1.5 shadow-sm dark:bg-zinc-900">
<Bar width={`${70 + ((i + c) % 3) * 10}%`} />
{c === 1 && i === 0 ? <span className={cn("block h-1.5 w-8 rounded-full", accent.solid)} /> : null}
</div>
))}
</div>
))}
</div>
</Window>
);
}
function CalendarScreen({ accent }: { accent: Accent }) {
const events: Record<number, string> = { 4: "Review", 11: "Launch", 17: "Retro", 24: "Ship" };
return (
<Window title="July">
<div className="grid grid-cols-7 gap-0.5 text-center text-[8px]">
{["M", "T", "W", "T", "F", "S", "S"].map((day, i) => (
<span key={i} className="pb-0.5 text-zinc-400">
{day}
</span>
))}
{Array.from({ length: 28 }, (_, i) => (
<div key={i} className="flex aspect-square flex-col items-center gap-0.5 rounded bg-zinc-50 pt-0.5 dark:bg-zinc-800/60">
<span className="text-zinc-500 tabular-nums">{i + 1}</span>
{events[i + 1] ? <span className={cn("h-1 w-3/4 rounded-full", accent.solid)} /> : null}
</div>
))}
</div>
</Window>
);
}
function TimelineScreen({ accent }: { accent: Accent }) {
const rows = [
{ name: "Research", start: 0, span: 30 },
{ name: "Design", start: 22, span: 34 },
{ name: "Build", start: 48, span: 40 },
{ name: "Launch", start: 84, span: 14 },
];
return (
<Window title="Timeline · Q3">
<div className="space-y-2.5">
{rows.map((row, i) => (
<div key={row.name} className="flex items-center gap-2">
<span className="w-12 shrink-0 text-zinc-500">{row.name}</span>
<div className="relative h-3 flex-1 rounded bg-zinc-100 dark:bg-zinc-800">
<span
className={cn("absolute inset-y-0 rounded", i === 2 ? accent.solid : accent.soft)}
style={{ left: `${row.start}%`, width: `${row.span}%` }}
/>
</div>
</div>
))}
</div>
</Window>
);
}
function PaletteScreen({ accent }: { accent: Accent }) {
const actions = ["New document", "Go to Insights", "Invite people", "Toggle dark mode"];
return (
<div className="grid size-full place-items-center bg-zinc-900/5 p-4 text-[10px] dark:bg-black/40">
<div className="w-full max-w-56 overflow-hidden rounded-xl bg-white shadow-xl ring-1 ring-zinc-900/10 dark:bg-zinc-900 dark:ring-white/10">
<div className="flex items-center gap-2 border-b border-zinc-200 px-2.5 py-2 dark:border-zinc-800">
<span className="text-zinc-400">Search or jump to…</span>
<span className="ml-auto">
<Kbd>⌘K</Kbd>
</span>
</div>
<div className="p-1">
{actions.map((action, i) => (
<div key={action} className={cn("flex items-center gap-2 rounded-md px-2 py-1.5", i === 0 && accent.soft)}>
<span className="size-2.5 rounded-sm border border-zinc-300 dark:border-zinc-600" />
{action}
</div>
))}
</div>
</div>
</div>
);
}
function ShortcutsScreen() {
const shortcuts = [
["Search", ["⌘", "K"]],
["New page", ["⌘", "N"]],
["Comment", ["⌘", "⇧", "M"]],
["Bold", ["⌘", "B"]],
["Go to board", ["G", "B"]],
["Help", ["?"]],
] as const;
return (
<Window title="Shortcuts">
<div className="space-y-2">
{shortcuts.map(([name, keys]) => (
<div key={name} className="flex items-center justify-between gap-2">
<span>{name}</span>
<span className="flex gap-0.5">
{keys.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</span>
</div>
))}
</div>
</Window>
);
}
function ThemeScreen({ accent }: { accent: Accent }) {
return (
<div className="grid size-full grid-cols-2 text-[10px]">
{[false, true].map((dark) => (
<div
key={String(dark)}
className={cn("flex flex-col gap-1.5 p-3", dark ? "bg-zinc-900 text-zinc-100" : "bg-white text-zinc-900")}
>
<p className="font-semibold">{dark ? "Dark" : "Light"}</p>
<span className={cn("block h-1.5 w-11/12 rounded-full", dark ? "bg-zinc-700" : "bg-zinc-200")} />
<span className={cn("block h-1.5 w-2/3 rounded-full", dark ? "bg-zinc-700" : "bg-zinc-200")} />
<span className={cn("mt-auto block h-4 w-10 rounded-full", accent.solid)} />
</div>
))}
</div>
);
}
function MembersScreen({ accent }: { accent: Accent }) {
const members = [
{ who: "MA", name: "Maya Ito", color: "bg-orange-400", role: "Owner" },
{ who: "JO", name: "Jonas Berg", color: "bg-sky-500", role: "Admin" },
{ who: "RI", name: "Rin Okafor", color: "bg-fuchsia-500", role: "Member" },
{ who: "LU", name: "Luca Sato", color: "bg-emerald-500", role: "Member" },
];
return (
<Window title="Members">
<div className="space-y-2">
{members.map((member) => (
<div key={member.who} className="flex items-center gap-2">
<Avatar name={member.who} className={member.color} />
<span className="min-w-0 flex-1 truncate font-medium">{member.name}</span>
<span
className={cn(
"rounded-full px-1.5 py-0.5",
member.role === "Owner" ? accent.badge : "bg-zinc-100 text-zinc-500 dark:bg-zinc-800",
)}
>
{member.role}
</span>
</div>
))}
</div>
</Window>
);
}
function InviteScreen({ accent }: { accent: Accent }) {
return (
<div className="grid size-full place-items-center bg-zinc-100 p-4 text-[10px] dark:bg-zinc-800">
<div className="w-full max-w-52 rounded-xl bg-white p-3 shadow-lg dark:bg-zinc-900">
<p className="text-[12px] font-semibold">Invite to Acme</p>
<div className="mt-2 flex flex-wrap gap-1 rounded-md border border-zinc-200 p-1.5 dark:border-zinc-700">
{["rin@acme.co", "luca@acme.co", "ana@acme.co"].map((email) => (
<span key={email} className={cn("rounded px-1.5 py-0.5", accent.badge)}>
{email}
</span>
))}
</div>
<div className="mt-2 flex gap-1.5">
<span className="flex-1 rounded-md border border-zinc-200 px-2 py-1.5 text-zinc-500 dark:border-zinc-700">
Member ▾
</span>
<span className={cn("rounded-md px-2 py-1.5 font-medium text-white", accent.solid)}>Send</span>
</div>
</div>
</div>
);
}
function AuditScreen() {
const entries = [
["09:41", "Maya changed Rin's role to Admin"],
["09:12", "Jonas exported Insights as PDF"],
["08:57", "Luca joined the workspace"],
["08:30", "Maya turned on SSO"],
];
return (
<Window title="Audit log">
<div className="divide-y divide-zinc-100 dark:divide-zinc-800">
{entries.map(([time, text]) => (
<div key={time} className="flex gap-3 py-1.5">
<span className="font-mono text-zinc-400 tabular-nums">{time}</span>
<span className="truncate">{text}</span>
</div>
))}
</div>
</Window>
);
}
Live preview, examples and props: https://keyframeui.com/components/playhead-reel#playhead-reel-changelog-demo























