# Stretch Index: Podcast Episodes

> 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.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/stretch-index-episodes-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/stretch-index-episodes-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/stretch-index-episodes-demo-light-af2966eebb8368b4.png
- Registry item: https://keyframeui.com/r/stretch-index-episodes-demo.json
- Categories: list, hover, layout
- Listed: 2026-10-14, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/stretch-index-episodes-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/stretch-index.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/stretch-index#stretch-index-episodes-demo

## Files

### `registry/demos/stretch-index-episodes-demo.tsx`

```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>
  );
}
```
