Web InnoventixFreeCode

Media Video Card

Original · free

video thumbnail card with play overlay

byWeb InnoventixReact + Tailwind
mediavideocard
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/media-video-card.json
media-video-card.tsx
"use client";

import {
  useCallback,
  useEffect,
  useId,
  useRef,
  useState,
  type KeyboardEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Chapter = { at: number; label: string };

type Video = {
  id: string;
  title: string;
  episode: string;
  posted: string;
  views: string;
  duration: number;
  watched: number;
  blurb: string;
  thumb: string;
  alt: string;
  chapters: Chapter[];
};

const VIDEOS: Video[] = [
  {
    id: "kb-11",
    title: "The last hand-cut dovetail shop in Rotherham",
    episode: "Episode 11",
    posted: "4 Jun 2026",
    views: "182K views",
    duration: 862,
    watched: 0,
    blurb:
      "Alan Petrie has cut every joint in this workshop by hand since 1978. No router jigs, no CNC. We spent three days watching him build a tool chest and asking the question his apprentices ask: why not just buy the machine?",
    thumb: "/img/gallery/g07.webp",
    alt: "A woodworker's bench lit from a side window, hand tools laid out across the surface",
    chapters: [
      { at: 0, label: "The shop at 6am" },
      { at: 148, label: "Why the machine never arrived" },
      { at: 361, label: "Cutting the tails" },
      { at: 604, label: "What his apprentices get wrong" },
      { at: 790, label: "Closing the chest" },
    ],
  },
  {
    id: "kb-10",
    title: "Why this potter fires at 1280°C and not a degree less",
    episode: "Episode 10",
    posted: "21 May 2026",
    views: "97K views",
    duration: 587,
    watched: 247,
    blurb:
      "Meera Sandhu lost an entire kiln load in February to a thermocouple that drifted by eleven degrees. She walks us through the firing curve she rebuilt from scratch, and the two hours where nothing can go wrong.",
    thumb: "/img/gallery/g19.webp",
    alt: "Rows of unglazed ceramic vessels waiting beside a kiln",
    chapters: [
      { at: 0, label: "Loading the kiln" },
      { at: 96, label: "The February failure" },
      { at: 254, label: "Reading the cones" },
      { at: 430, label: "Opening day" },
    ],
  },
  {
    id: "kb-09",
    title: "Rebuilding a 1953 Wurlitzer, one felt at a time",
    episode: "Episode 09",
    posted: "8 May 2026",
    views: "241K views",
    duration: 1266,
    watched: 1089,
    blurb:
      "Sixty-one hammers, every one of them hardened past use. Tomas Beltrán has restored 340 pianos and still refuses to voice a hammer before the action is regulated. This is the long version, uncut.",
    thumb: "/img/gallery/g28.webp",
    alt: "The exposed action of a vintage electric piano with hammers and dampers visible",
    chapters: [
      { at: 0, label: "What arrived on the truck" },
      { at: 212, label: "Pulling the action" },
      { at: 545, label: "Regulate first, voice second" },
      { at: 918, label: "First chord in forty years" },
    ],
  },
  {
    id: "kb-08",
    title: "The blacksmith who only works in winter",
    episode: "Episode 08",
    posted: "23 Apr 2026",
    views: "64K views",
    duration: 758,
    watched: 0,
    blurb:
      "Ingrid Halvorsen shuts the forge from May to September. Not for holiday — because she reads colour off the steel by eye, and summer light through the door lies to her about temperature.",
    thumb: "/img/gallery/g12.webp",
    alt: "A dim forge interior with a glowing bar of steel resting on an anvil",
    chapters: [
      { at: 0, label: "Six months of dark" },
      { at: 175, label: "Reading heat by colour" },
      { at: 402, label: "Drawing out the taper" },
      { at: 640, label: "Why summer lies" },
    ],
  },
];

function formatTime(seconds: number): string {
  const total = Math.max(0, Math.floor(seconds));
  const m = Math.floor(total / 60);
  const s = total % 60;
  return `${m}:${String(s).padStart(2, "0")}`;
}

function spokenTime(seconds: number): string {
  const total = Math.max(0, Math.floor(seconds));
  const m = Math.floor(total / 60);
  const s = total % 60;
  return `${m} minute${m === 1 ? "" : "s"} ${s} second${s === 1 ? "" : "s"}`;
}

function PlayGlyph({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" aria-hidden="true" className={className}>
      <path d="M8 5.14v13.72a.6.6 0 0 0 .92.5l10.7-6.86a.6.6 0 0 0 0-1l-10.7-6.86a.6.6 0 0 0-.92.5Z" fill="currentColor" />
    </svg>
  );
}

function PauseGlyph({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" aria-hidden="true" className={className}>
      <rect x="6.5" y="5" width="3.6" height="14" rx="1.2" fill="currentColor" />
      <rect x="13.9" y="5" width="3.6" height="14" rx="1.2" fill="currentColor" />
    </svg>
  );
}

function SoundOnGlyph({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M11 5 6.5 8.8H3.4v6.4h3.1L11 19Z" />
      <path d="M15.2 9.3a3.7 3.7 0 0 1 0 5.4" />
      <path d="M17.9 6.5a7.5 7.5 0 0 1 0 11" />
    </svg>
  );
}

function SoundOffGlyph({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M11 5 6.5 8.8H3.4v6.4h3.1L11 19Z" />
      <path d="m15.4 9.6 5 4.8" />
      <path d="m20.4 9.6-5 4.8" />
    </svg>
  );
}

function BookmarkGlyph({ filled, className }: { filled: boolean; className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M6.5 4.6h11a1 1 0 0 1 1 1v13.6a.5.5 0 0 1-.78.41L12 15.6l-5.72 4.01a.5.5 0 0 1-.78-.41V5.6a1 1 0 0 1 1-1Z" />
    </svg>
  );
}

export default function MediaVideoCard() {
  const reduce = useReducedMotion();
  const seekId = useId();
  const listId = useId();

  const [activeIndex, setActiveIndex] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [time, setTime] = useState(0);
  const [muted, setMuted] = useState(false);
  const [saved, setSaved] = useState<string[]>(["kb-09"]);
  const [scrubbing, setScrubbing] = useState(false);

  const barRef = useRef<HTMLDivElement | null>(null);
  const timeRef = useRef(0);
  const active = VIDEOS[activeIndex];
  const ratio = active.duration === 0 ? 0 : time / active.duration;
  const isSaved = saved.includes(active.id);

  const commitTime = useCallback((next: number) => {
    timeRef.current = next;
    setTime(next);
  }, []);

  useEffect(() => {
    if (!playing) return;
    const duration = active.duration;
    const id = window.setInterval(() => {
      const next = Math.min(duration, timeRef.current + 0.25);
      timeRef.current = next;
      setTime(next);
      if (next >= duration) setPlaying(false);
    }, 250);
    return () => window.clearInterval(id);
  }, [playing, active.duration]);

  const selectVideo = useCallback(
    (index: number) => {
      setActiveIndex(index);
      commitTime(VIDEOS[index].watched);
      setPlaying(false);
    },
    [commitTime],
  );

  const seekToClientX = useCallback(
    (clientX: number) => {
      const el = barRef.current;
      if (!el) return;
      const rect = el.getBoundingClientRect();
      if (rect.width === 0) return;
      const next = (clientX - rect.left) / rect.width;
      commitTime(Math.min(1, Math.max(0, next)) * active.duration);
    },
    [active.duration, commitTime],
  );

  const onSeekKeyDown = useCallback(
    (event: KeyboardEvent<HTMLDivElement>) => {
      const step =
        event.key === "ArrowLeft" || event.key === "ArrowRight" ? 5 : 15;
      let next: number | null = null;
      if (event.key === "ArrowLeft" || event.key === "ArrowDown") next = time - step;
      else if (event.key === "ArrowRight" || event.key === "ArrowUp") next = time + step;
      else if (event.key === "Home") next = 0;
      else if (event.key === "End") next = active.duration;
      if (next === null) return;
      event.preventDefault();
      commitTime(Math.min(active.duration, Math.max(0, next)));
    },
    [time, active.duration, commitTime],
  );

  const toggleSaved = useCallback(() => {
    setSaved((list) =>
      list.includes(active.id)
        ? list.filter((id) => id !== active.id)
        : [...list, active.id],
    );
  }, [active.id]);

  const fade = reduce
    ? { duration: 0 }
    : { duration: 0.45, ease: [0.22, 1, 0.36, 1] as const };

  return (
    <section className="relative w-full bg-slate-50 px-4 py-20 sm:px-6 sm:py-28 dark:bg-slate-950">
      <style>{`
        @keyframes mvc-ring {
          0% { transform: scale(1); opacity: 0.55; }
          70% { transform: scale(1.7); opacity: 0; }
          100% { transform: scale(1.7); opacity: 0; }
        }
        @keyframes mvc-eq {
          0%, 100% { transform: scaleY(0.28); }
          50% { transform: scaleY(1); }
        }
        .mvc-ring { animation: mvc-ring 2.6s cubic-bezier(0.22, 1, 0.36, 1) infinite; }
        .mvc-eq-bar { animation: mvc-eq 900ms ease-in-out infinite; transform-origin: bottom; }
        @media (prefers-reduced-motion: reduce) {
          .mvc-ring, .mvc-eq-bar { animation: none !important; }
          .mvc-ring { opacity: 0; }
          .mvc-eq-bar { transform: scaleY(0.6); }
        }
      `}</style>

      <div className="mx-auto w-full max-w-6xl">
        <header className="max-w-2xl">
          <p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
            Kiln &amp; Bench — Season 2
          </p>
          <h2 className="mt-3 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
            Short films about people who still make things by hand
          </h2>
          <p className="mt-4 text-base leading-relaxed text-slate-600 dark:text-slate-400">
            Four episodes, filmed on location across two winters. No sponsors, no
            b-roll library — just long takes of work that takes a long time.
          </p>
        </header>

        <div className="mt-12 grid gap-6 lg:grid-cols-[1.6fr_1fr]">
          <article className="overflow-hidden rounded-3xl bg-white shadow-sm ring-1 ring-slate-200 dark:bg-slate-900 dark:ring-white/10">
            <div className="group relative aspect-video overflow-hidden bg-slate-900">
              <AnimatePresence initial={false}>
                <motion.div
                  key={active.id}
                  initial={{ opacity: 0, scale: reduce ? 1 : 1.04 }}
                  animate={{ opacity: 1, scale: 1 }}
                  exit={{ opacity: 0 }}
                  transition={fade}
                  className="absolute inset-0"
                >
                  {/* eslint-disable-next-line @next/next/no-img-element */}
                  <img
                    src={active.thumb}
                    alt={active.alt}
                    loading="lazy"
                    draggable={false}
                    className="h-full w-full object-cover"
                  />
                </motion.div>
              </AnimatePresence>

              <div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-slate-950/85 via-slate-950/10 to-slate-950/35" />

              <div className="pointer-events-none absolute left-4 top-4 flex items-center gap-2">
                <span className="rounded-full bg-slate-950/70 px-2.5 py-1 text-[11px] font-medium uppercase tracking-wider text-white/90 backdrop-blur-sm">
                  {active.episode}
                </span>
                {playing ? (
                  <span className="flex items-center gap-1.5 rounded-full bg-rose-600/90 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider text-white backdrop-blur-sm">
                    <svg viewBox="0 0 12 12" className="h-3 w-3" aria-hidden="true">
                      <rect className="mvc-eq-bar" x="1" y="2" width="2" height="8" rx="1" fill="currentColor" style={{ animationDelay: "0ms" }} />
                      <rect className="mvc-eq-bar" x="5" y="2" width="2" height="8" rx="1" fill="currentColor" style={{ animationDelay: "160ms" }} />
                      <rect className="mvc-eq-bar" x="9" y="2" width="2" height="8" rx="1" fill="currentColor" style={{ animationDelay: "320ms" }} />
                    </svg>
                    Playing
                  </span>
                ) : null}
              </div>

              <span className="pointer-events-none absolute right-4 top-4 rounded-md bg-slate-950/75 px-2 py-1 font-mono text-xs font-medium text-white/95 tabular-nums backdrop-blur-sm">
                {formatTime(active.duration)}
              </span>

              <div className="absolute inset-0 flex items-center justify-center">
                <span className="relative flex h-20 w-20 items-center justify-center">
                  {!playing && !reduce ? (
                    <span className="mvc-ring pointer-events-none absolute inset-0 rounded-full bg-white/70" />
                  ) : null}
                  <button
                    type="button"
                    onClick={() => setPlaying((p) => !p)}
                    aria-label={
                      playing
                        ? `Pause “${active.title}”`
                        : `Play “${active.title}”, ${spokenTime(active.duration)}`
                    }
                    className={[
                      "relative flex h-16 w-16 items-center justify-center rounded-full text-slate-900 shadow-xl shadow-slate-950/40 outline-none ring-1 ring-white/60 backdrop-blur transition duration-300",
                      "bg-white/95 hover:scale-105 hover:bg-white active:scale-95",
                      "focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900",
                      "motion-reduce:transition-none motion-reduce:hover:scale-100 motion-reduce:active:scale-100",
                      playing
                        ? "opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
                        : "opacity-100",
                    ].join(" ")}
                  >
                    {playing ? (
                      <PauseGlyph className="h-7 w-7" />
                    ) : (
                      <PlayGlyph className="ml-0.5 h-7 w-7" />
                    )}
                  </button>
                </span>
              </div>

              <div className="pointer-events-none absolute inset-x-0 bottom-0 px-5 pb-4">
                <h3 className="text-lg font-semibold leading-snug text-white drop-shadow sm:text-xl">
                  {active.title}
                </h3>
                <p className="mt-1 text-xs text-white/70">
                  {active.views} · {active.posted}
                </p>
              </div>

              <div className="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 bg-white/20">
                <div
                  className="h-full bg-rose-500"
                  style={{ width: `${ratio * 100}%` }}
                />
              </div>
            </div>

            <div className="border-t border-slate-200 px-5 py-4 dark:border-white/10">
              <div className="flex items-center gap-3">
                <button
                  type="button"
                  onClick={() => setPlaying((p) => !p)}
                  aria-label={playing ? "Pause" : "Play"}
                  className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-slate-900 text-white outline-none transition hover:bg-slate-700 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-offset-slate-900"
                >
                  {playing ? (
                    <PauseGlyph className="h-4 w-4" />
                  ) : (
                    <PlayGlyph className="ml-0.5 h-4 w-4" />
                  )}
                </button>

                <span className="w-11 shrink-0 font-mono text-xs text-slate-500 tabular-nums dark:text-slate-400">
                  {formatTime(time)}
                </span>

                <div
                  ref={barRef}
                  id={seekId}
                  role="slider"
                  tabIndex={0}
                  aria-label="Seek"
                  aria-valuemin={0}
                  aria-valuemax={active.duration}
                  aria-valuenow={Math.floor(time)}
                  aria-valuetext={`${spokenTime(time)} of ${spokenTime(active.duration)}`}
                  onKeyDown={onSeekKeyDown}
                  onPointerDown={(event) => {
                    event.currentTarget.setPointerCapture(event.pointerId);
                    setScrubbing(true);
                    seekToClientX(event.clientX);
                  }}
                  onPointerMove={(event) => {
                    if (scrubbing) seekToClientX(event.clientX);
                  }}
                  onPointerUp={(event) => {
                    event.currentTarget.releasePointerCapture(event.pointerId);
                    setScrubbing(false);
                  }}
                  onPointerCancel={() => setScrubbing(false)}
                  className="group relative h-8 flex-1 cursor-pointer touch-none select-none rounded-md outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
                >
                  <span className="pointer-events-none absolute inset-x-0 top-1/2 h-1.5 -translate-y-1/2 overflow-hidden rounded-full bg-slate-200 dark:bg-white/15">
                    <span
                      className="absolute inset-y-0 left-0 rounded-full bg-rose-500"
                      style={{ width: `${ratio * 100}%` }}
                    />
                  </span>
                  {active.chapters.slice(1).map((chapter) => (
                    <span
                      key={chapter.at}
                      className="pointer-events-none absolute top-1/2 h-1.5 w-0.5 -translate-y-1/2 bg-white/90 dark:bg-slate-900/80"
                      style={{ left: `${(chapter.at / active.duration) * 100}%` }}
                    />
                  ))}
                  <span
                    className="pointer-events-none absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-rose-500 shadow ring-2 ring-white transition-transform group-hover:scale-125 motion-reduce:transition-none dark:ring-slate-900"
                    style={{ left: `${ratio * 100}%` }}
                  />
                </div>

                <span className="w-11 shrink-0 text-right font-mono text-xs text-slate-500 tabular-nums dark:text-slate-400">
                  −{formatTime(active.duration - time)}
                </span>

                <button
                  type="button"
                  onClick={() => setMuted((m) => !m)}
                  aria-pressed={muted}
                  aria-label="Mute audio"
                  className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-slate-500 outline-none transition hover:bg-slate-100 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none dark:text-slate-400 dark:hover:bg-white/10 dark:hover:text-white dark:focus-visible:ring-offset-slate-900"
                >
                  {muted ? (
                    <SoundOffGlyph className="h-4.5 w-4.5" />
                  ) : (
                    <SoundOnGlyph className="h-4.5 w-4.5" />
                  )}
                </button>

                <button
                  type="button"
                  onClick={toggleSaved}
                  aria-pressed={isSaved}
                  aria-label={`Save “${active.title}” to watch later`}
                  className={[
                    "flex h-9 w-9 shrink-0 items-center justify-center rounded-full outline-none transition hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none dark:hover:bg-white/10 dark:focus-visible:ring-offset-slate-900",
                    isSaved
                      ? "text-indigo-600 dark:text-indigo-400"
                      : "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white",
                  ].join(" ")}
                >
                  <BookmarkGlyph filled={isSaved} className="h-4.5 w-4.5" />
                </button>
              </div>

              <div className="mt-4 flex flex-wrap gap-1.5">
                {active.chapters.map((chapter) => {
                  const current =
                    time >= chapter.at &&
                    time <
                      (active.chapters.find((c) => c.at > chapter.at)?.at ??
                        active.duration + 1);
                  return (
                    <button
                      key={chapter.at}
                      type="button"
                      onClick={() => commitTime(chapter.at)}
                      aria-current={current ? "true" : undefined}
                      className={[
                        "rounded-full px-2.5 py-1 text-xs outline-none transition focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none dark:focus-visible:ring-offset-slate-900",
                        current
                          ? "bg-indigo-600 font-medium text-white"
                          : "bg-slate-100 text-slate-600 hover:bg-slate-200 hover:text-slate-900 dark:bg-white/10 dark:text-slate-300 dark:hover:bg-white/20 dark:hover:text-white",
                      ].join(" ")}
                    >
                      <span className="font-mono tabular-nums opacity-60">
                        {formatTime(chapter.at)}
                      </span>{" "}
                      {chapter.label}
                    </button>
                  );
                })}
              </div>

              <p className="mt-4 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
                {active.blurb}
              </p>
            </div>
          </article>

          <div className="flex flex-col">
            <h3
              id={listId}
              className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400"
            >
              Up next
            </h3>
            <ul aria-labelledby={listId} className="mt-3 flex flex-col gap-3">
              {VIDEOS.map((video, index) => {
                const isActive = index === activeIndex;
                const progress =
                  isActive && time > 0
                    ? time / video.duration
                    : video.watched / video.duration;
                return (
                  <li key={video.id}>
                    <button
                      type="button"
                      onClick={() => selectVideo(index)}
                      aria-current={isActive ? "true" : undefined}
                      className={[
                        "group flex w-full gap-3 rounded-2xl p-2 text-left outline-none transition focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 motion-reduce:transition-none dark:focus-visible:ring-offset-slate-950",
                        isActive
                          ? "bg-white shadow-sm ring-1 ring-indigo-500/40 dark:bg-slate-900 dark:ring-indigo-400/40"
                          : "hover:bg-white/70 dark:hover:bg-white/5",
                      ].join(" ")}
                    >
                      <span className="relative aspect-video w-32 shrink-0 overflow-hidden rounded-xl bg-slate-900 sm:w-36">
                        {/* eslint-disable-next-line @next/next/no-img-element */}
                        <img
                          src={video.thumb}
                          alt={video.alt}
                          loading="lazy"
                          draggable={false}
                          className="h-full w-full object-cover transition duration-500 group-hover:scale-105 motion-reduce:transition-none motion-reduce:group-hover:scale-100"
                        />
                        <span className="absolute inset-0 bg-slate-950/30 transition group-hover:bg-slate-950/10 motion-reduce:transition-none" />
                        <span className="absolute inset-0 flex items-center justify-center">
                          <span className="flex h-8 w-8 items-center justify-center rounded-full bg-white/90 text-slate-900 opacity-0 shadow transition duration-300 group-hover:opacity-100 group-focus-visible:opacity-100 motion-reduce:transition-none">
                            <PlayGlyph className="ml-0.5 h-3.5 w-3.5" />
                          </span>
                        </span>
                        <span className="absolute bottom-1 right-1 rounded bg-slate-950/80 px-1.5 py-0.5 font-mono text-[10px] font-medium text-white tabular-nums">
                          {formatTime(video.duration)}
                        </span>
                        {progress > 0 ? (
                          <span className="absolute inset-x-0 bottom-0 h-1 bg-white/25">
                            <span
                              className="block h-full bg-rose-500"
                              style={{ width: `${Math.min(1, progress) * 100}%` }}
                            />
                          </span>
                        ) : null}
                      </span>

                      <span className="min-w-0 flex-1 py-0.5">
                        <span className="flex items-center gap-1.5">
                          <span className="text-[11px] font-medium uppercase tracking-wider text-slate-400 dark:text-slate-500">
                            {video.episode}
                          </span>
                          {saved.includes(video.id) ? (
                            <BookmarkGlyph
                              filled
                              className="h-3 w-3 text-indigo-500 dark:text-indigo-400"
                            />
                          ) : null}
                        </span>
                        <span
                          className={[
                            "mt-0.5 block text-sm font-medium leading-snug",
                            isActive
                              ? "text-indigo-700 dark:text-indigo-300"
                              : "text-slate-900 dark:text-slate-100",
                          ].join(" ")}
                        >
                          {video.title}
                        </span>
                        <span className="mt-1 block text-xs text-slate-500 dark:text-slate-400">
                          {video.views} · {video.posted}
                        </span>
                      </span>
                    </button>
                  </li>
                );
              })}
            </ul>

            <p className="mt-4 rounded-2xl bg-white/60 p-3 text-xs leading-relaxed text-slate-500 ring-1 ring-slate-200 dark:bg-white/5 dark:text-slate-400 dark:ring-white/10">
              Episodes 1–7 are archived on the Season 1 page. New films land the
              second Thursday of every month.
            </p>
          </div>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →