"use client"; import React, { forwardRef, useEffect, useMemo, useRef } from "react"; import { Img } from "@page-speed/img"; import type { MediaItem } from "../types/index.js"; import { formatDuration } from "../utils/formatDuration.js"; import { prefersReducedMotion } from "../utils/prefersReducedMotion.js"; /** * Preset thumbnail sizes. * * - `sm` — 88px wide (used in the "AI-generated reel" callout in the design) * - `md` — 152px wide (used in the horizontal carousel row) * - `hero` — 200px wide (a heavier featured thumbnail) */ export type ThumbnailSize = "sm" | "md" | "hero" | number; /** * Props for ``. * * The card renders a 9:16 poster with overlay chrome (badge, title, duration, * audio-bars glyph, progress hint). Tapping/clicking it calls * `onOpen(item.id)`. * * The card is intentionally "dumb" — it does not know about feed state. When * rendered inside `` (or ``), pass * `onOpen` = the provider's `open` function; otherwise supply your own. */ export interface ThumbnailCardProps { item: MediaItem; /** Called with the item id when the card is activated. */ onOpen: (id: string) => void; /** Preset size, or a raw pixel width. Default `"md"`. */ size?: ThumbnailSize; /** * Show the small muted-speaker icon in the top-right of the card. * Default in v0.2+: hidden. At thumbnail sizes, the audio-bars glyph in * the caption already communicates that video has sound; the extra icon * only adds noise. In v0.1 the icon was on by default (opt-out via * `hideMutedIcon`); that behavior can be restored with `showMutedIcon`. */ showMutedIcon?: boolean; /** * @deprecated Prefer `showMutedIcon={false}` (which is now the default). * Retained for backwards compatibility with v0.1.x consumers. When * `showMutedIcon` is unset and `hideMutedIcon` is provided, the boolean * is inverted. */ hideMutedIcon?: boolean; /** Hide the bottom title/duration overlay. */ hideCaption?: boolean; /** Hide the animated progress hint line at the bottom. */ hideProgressHint?: boolean; /** * When true (default), the card plays a muted, looping preview of the * video instead of showing a static poster. Automatically disabled when * the user has `prefers-reduced-motion: reduce`, or when the item has no * video source. Falls back to `poster` in both cases. */ autoplayPreview?: boolean; /** * Elevate the card with a stronger drop-shadow. Default `true`. Set false * for flat layouts or when the card is inside a container that already * provides elevation (e.g. the section-level "AI-generated reel" callout). */ elevated?: boolean; /** Additional class name applied to the outer element. */ className?: string; /** Additional inline style applied to the outer element. */ style?: React.CSSProperties; } function resolveWidth(size: ThumbnailSize): number { if (typeof size === "number") return size; if (size === "sm") return 88; if (size === "hero") return 200; return 152; // "md" } function resolveBorderRadius(size: ThumbnailSize): number { const w = resolveWidth(size); if (w <= 100) return 11; if (w <= 160) return 16; return 18; } /** * A single tappable thumbnail representing one MediaItem. */ export const ThumbnailCard = forwardRef( function ThumbnailCard( { item, onOpen, size = "md", showMutedIcon, hideMutedIcon, hideCaption, hideProgressHint, autoplayPreview = true, elevated = true, className, style, }, ref, ) { const width = resolveWidth(size); const radius = resolveBorderRadius(size); const durationLabel = item.durationLabel ?? (item.durationMs != null ? formatDuration(item.durationMs) : ""); // Decide the visual: autoplay preview (muted looping