A podcast's latest episodes on a dark panel, with square covers through `imageAspect={1}` and episode numbers through `number`. With `collapseOnLeave`, the list folds shut when the pointer leaves it.
@keyframeui
registry/demos/stretch-index-episodes-demo.tsx
"use client";
import * as React from "react";
import { StretchIndex, StretchIndexItem } from "@/components/ui/stretch-index";
const episodes = [
{
number: "24",
title: "Springs, and why nothing should stop dead",
guest: "Ana Ruiz, motion lead at Tessel",
text: "Why a spring that settles beats a curve that ends, and the three numbers Ana tunes first.",
length: "52 min",
photo: "1617791160505-6f00504e3519",
accent: "#8b7cf6",
},
{
number: "23",
title: "Designing the empty state",
guest: "Kofi Mensah, product designer at Tally",
text: "The first screen most people see is the one with nothing on it. How to make it teach, not apologize.",
length: "41 min",
photo: "1634017839464-5c339ebe3cb4",
accent: "#3b82f6",
},
{
number: "22",
title: "Type that moves",
guest: "Lena Hart, type designer",
text: "Variable fonts, kinetic headlines, and when animating letters helps reading instead of hurting it.",
length: "47 min",
photo: "1620641788421-7a1c342ea42e",
accent: "#ec4899",
},
{
number: "21",
title: "The case for fewer animations",
guest: "Sam Okoro, design director at Quill",
text: "Sam cut 60% of his app's transitions and support tickets went down. What stayed, and why.",
length: "38 min",
photo: "1579546929518-9e396f3cc809",
accent: "#f59e0b",
},
{
number: "20",
title: "Prototyping in the browser",
guest: "Mira Chen, design engineer at Parcel",
text: "Skipping the mockup and sketching straight in code: what you gain, and the habits that keep it fast.",
length: "55 min",
photo: "1604076913837-52ab5629fba9",
accent: "#ef4444",
},
];
const photo = (id: string) =>
`https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=320&h=320&q=80`;
export function StretchIndexEpisodesDemo() {
const [playing, setPlaying] = React.useState<string | null>(null);
return (
<section className="w-full max-w-3xl rounded-3xl bg-zinc-950 p-5 text-zinc-100 ring-1 ring-white/10 sm:p-8">
<div className="mb-6 flex items-end justify-between gap-4">
<div>
<p className="font-mono text-xs tracking-widest text-zinc-400 uppercase">Frame Rate</p>
<h3 className="mt-2 text-2xl font-semibold tracking-tight">Latest episodes</h3>
</div>
<p className="font-mono text-xs text-zinc-400">Every other Tuesday</p>
</div>
<StretchIndex
collapseOnLeave
imageAspect={1}
imageWidth="clamp(4.5rem, 18cqi, 8.5rem)"
className="border-t border-current/10"
>
{episodes.map((episode) => {
const isPlaying = playing === episode.number;
return (
<StretchIndexItem
key={episode.number}
number={episode.number}
title={episode.title}
meta={episode.length}
image={photo(episode.photo)}
accent={episode.accent}
>
<p className="text-sm text-zinc-400">With {episode.guest}</p>
<p className="mt-1 max-w-md text-pretty text-zinc-300">{episode.text}</p>
<button
type="button"
aria-pressed={isPlaying}
onClick={() => setPlaying(isPlaying ? null : episode.number)}
className="mt-3 inline-flex h-8 items-center gap-2 rounded-full bg-white/10 pr-3.5 pl-2 text-sm font-medium text-white outline-none hover:bg-white/15 focus-visible:ring-2 focus-visible:ring-white/70"
>
<span
aria-hidden
className="grid size-5 place-items-center rounded-full"
style={{ backgroundColor: episode.accent }}
>
{isPlaying ? (
<svg viewBox="0 0 10 10" className="size-2.5 fill-white">
<rect x="1.5" y="1" width="2.5" height="8" rx="0.6" />
<rect x="6" y="1" width="2.5" height="8" rx="0.6" />
</svg>
) : (
<svg viewBox="0 0 10 10" className="ml-px size-2.5 fill-white">
<path d="M2 1.2v7.6a.6.6 0 0 0 .9.5l6.2-3.8a.6.6 0 0 0 0-1L2.9.7a.6.6 0 0 0-.9.5Z" />
</svg>
)}
</span>
{isPlaying ? "Pause" : "Play episode"}
</button>
</StretchIndexItem>
);
})}
</StretchIndex>
</section>
);
}
Live preview, examples and props: https://keyframeui.com/components/stretch-index#stretch-index-episodes-demo






















