import { useEffect, useRef, useState, startTransition } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { Play, Info, Flame, ChevronLeft, ChevronRight, Star, Clock, Tv2, Film } from '../../lib/icons' import type { useNavigate } from 'react-router-dom' import { formatRuntime } from '../../lib/format' import { getBestImage, getImageUrl, getStoredServerUrl } from '../../api/jellyfin' import { usePreferencesStore } from '../../stores/preferences-store' import type { BaseItemDto } from '../../api/types' import { Dot } from './home-utils' /* ───────────────────────────────────────────────────────────────── */ /* Featured Carousel - Ken Burns rotating hero */ /* ───────────────────────────────────────────────────────────────── */ export function FeaturedCarousel({ items, navigate, }: { items: BaseItemDto[] navigate: ReturnType }) { const [index, setIndex] = useState(0) const [paused, setPaused] = useState(false) const [progress, setProgress] = useState(0) // Track whether the user navigated forward (next) or backward (prev) so // the slide cross-fade has a slight directional drift. const directionRef = useRef<1 | -1>(1) const heroAutoAdvance = usePreferencesStore(s => s.heroAutoAdvance) const heroAutoAdvanceMs = usePreferencesStore(s => s.heroAutoAdvanceMs) const reduceMotion = usePreferencesStore(s => s.reduceMotion) const advanceDisabled = !heroAutoAdvance || reduceMotion const total = items.length const item = items[index] /* Auto-advance + smooth progress */ useEffect(() => { if (paused || advanceDisabled || total <= 1) { setProgress(0) return } setProgress(0) const start = Date.now() const id = setInterval(() => { const elapsed = Date.now() - start const p = Math.min(1, elapsed / heroAutoAdvanceMs) setProgress(p) if (p >= 1) { directionRef.current = 1 setIndex(i => (i + 1) % total) } }, 50) return () => clearInterval(id) }, [index, paused, total, advanceDisabled, heroAutoAdvanceMs]) /* Keyboard nav */ useEffect(() => { if (total <= 1) return function handle(e: KeyboardEvent) { const tag = (document.activeElement?.tagName || '').toLowerCase() if (tag === 'input' || tag === 'textarea' || tag === 'select') return if (e.key === 'ArrowLeft') { directionRef.current = -1 setIndex(i => (i - 1 + total) % total) } else if (e.key === 'ArrowRight') { directionRef.current = 1 setIndex(i => (i + 1) % total) } } window.addEventListener('keydown', handle) return () => window.removeEventListener('keydown', handle) }, [total]) function goPrev() { directionRef.current = -1 startTransition(() => setIndex(i => (i - 1 + total) % total)) } function goNext() { directionRef.current = 1 startTransition(() => setIndex(i => (i + 1) % total)) } function goTo(i: number) { directionRef.current = i > index ? 1 : -1 startTransition(() => setIndex(i)) } if (!item) return null return (
setPaused(true)} onMouseLeave={() => setPaused(false)} role="region" aria-label="Featured titles" aria-roledescription="carousel" > {/* Edge gradients to keep arrows + title legible regardless of backdrop */}
{/* Bottom gradient blend into rows */}
{/* Manual nav arrows - only when more than 1 slide */} {total > 1 && ( <> )} {/* Indicators - Netflix-style filling progress bars */} {total > 1 && (
{items.map((_, i) => ( ))}
{String(index + 1).padStart(2, '0')} / {String(total).padStart(2, '0')}
)}
) } function CarouselArrow({ side, onClick }: { side: 'left' | 'right'; onClick: () => void }) { const Icon = side === 'left' ? ChevronLeft : ChevronRight return ( ) } function FeaturedSlide({ item, navigate, direction, }: { item: BaseItemDto navigate: ReturnType direction: 1 | -1 }) { const serverUrl = getStoredServerUrl() const reduceMotion = usePreferencesStore(s => s.reduceMotion) const backdrop = getBestImage(serverUrl, item, 'backdrop', 1920) const logo = item.Id && item.ImageTags?.Logo ? getImageUrl(serverUrl, item.Id, 'Logo', 600, item.ImageTags.Logo) : null const title = item.Name || '' const overview = item.Overview || '' const year = item.ProductionYear const rating = item.OfficialRating const runtime = item.RunTimeTicks ? formatRuntime(item.RunTimeTicks) : null const genres = (item.Genres || []).slice(0, 3) const community = item.CommunityRating const progress = item.UserData?.PlayedPercentage const hasProgress = typeof progress === 'number' && progress > 0 const isSeries = item.Type === 'Series' const TypeIcon = isSeries ? Tv2 : Film const tagline = (item as { Taglines?: string[] }).Taglines?.[0] return ( {/* Ken Burns backdrop - slow scale + horizontal pan, disabled when reduce-motion is on */} {backdrop && ( )} {/* Multi-direction gradient masks for legibility */}
{/* Content */}
{/* Eyebrow */}
Featured {isSeries ? 'Series' : 'Movie'}
{/* Title - logo if available, else display text */} {logo ? ( {title} { (e.target as HTMLImageElement).style.display = 'none' }} /> ) : (

{title}

)} {tagline && (

"{tagline}"

)} {/* Meta line */}
{year && {year}} {year && (rating || runtime) && } {rating && ( {rating} )} {rating && runtime && } {runtime && ( {runtime} )} {community != null && community > 0 && ( <> {community.toFixed(1)} )}
{/* Genres */} {genres.length > 0 && (
{genres.map(g => ( {g} ))}
)} {/* Overview */} {overview && (

{overview}

)} {/* Actions */}
) }