Media Video Hero
Original · freehero with a video-style backdrop
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-hero.json"use client";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type Chapter = {
id: string;
label: string;
start: number;
blurb: string;
};
const DURATION = 214;
const CHAPTERS: Chapter[] = [
{
id: "cold-start",
label: "Cold start",
start: 0,
blurb: "4:12am at the Reykjanes wellhead. Sensors wake before the crew does.",
},
{
id: "pressure",
label: "Pressure curve",
start: 46,
blurb: "Steam hits 184 psi. The turbine hall goes from silent to deafening in nine seconds.",
},
{
id: "handoff",
label: "Grid handoff",
start: 118,
blurb: "Load balances across three substations. Nobody in the room touches a switch.",
},
{
id: "dusk",
label: "Dusk shift",
start: 171,
blurb: "Output holds at 61 MW while the field crew drives home in the dark.",
},
];
function formatTime(seconds: number): string {
const safe = Math.max(0, Math.floor(seconds));
const mins = Math.floor(safe / 60);
const secs = safe % 60;
return `${mins}:${secs.toString().padStart(2, "0")}`;
}
export default function MediaVideoHero() {
const reduceMotion = useReducedMotion();
const [playing, setPlaying] = useState(false);
const [muted, setMuted] = useState(true);
const [time, setTime] = useState(0);
const [scrubbing, setScrubbing] = useState(false);
const rafRef = useRef<number | null>(null);
const lastTickRef = useRef<number>(0);
const statusId = useId();
const captionId = useId();
const activeChapter = useMemo<Chapter>(() => {
let current = CHAPTERS[0];
for (const chapter of CHAPTERS) {
if (time >= chapter.start) current = chapter;
}
return current;
}, [time]);
useEffect(() => {
if (!playing || scrubbing) {
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
return;
}
lastTickRef.current = performance.now();
const tick = (now: number) => {
const delta = (now - lastTickRef.current) / 1000;
lastTickRef.current = now;
setTime((prev) => {
const next = prev + delta;
if (next >= DURATION) {
setPlaying(false);
return DURATION;
}
return next;
});
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => {
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
}, [playing, scrubbing]);
const togglePlay = useCallback(() => {
setPlaying((prev) => {
if (!prev && time >= DURATION) setTime(0);
return !prev;
});
}, [time]);
const jumpTo = useCallback((seconds: number) => {
setTime(Math.min(DURATION, Math.max(0, seconds)));
setPlaying(true);
}, []);
const progress = (time / DURATION) * 100;
return (
<section className="relative w-full overflow-hidden bg-slate-950 py-20 text-slate-100 sm:py-28 dark:bg-slate-950">
<style>{`
@keyframes mvh-drift {
0% { transform: translate3d(0, 0, 0) scale(1.08); }
50% { transform: translate3d(-2.5%, -1.5%, 0) scale(1.14); }
100% { transform: translate3d(0, 0, 0) scale(1.08); }
}
@keyframes mvh-scan {
0% { transform: translateY(-120%); opacity: 0; }
12% { opacity: 0.5; }
88% { opacity: 0.5; }
100% { transform: translateY(320%); opacity: 0; }
}
@keyframes mvh-flicker {
0%, 100% { opacity: 0.16; }
43% { opacity: 0.28; }
71% { opacity: 0.1; }
}
@keyframes mvh-pulse-ring {
0% { transform: scale(1); opacity: 0.55; }
100% { transform: scale(1.9); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.mvh-drift, .mvh-scan, .mvh-flicker, .mvh-ring {
animation: none !important;
}
.mvh-scan { opacity: 0 !important; }
}
`}</style>
{/* Video-style backdrop: layered gradient "frames" that drift like footage */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div
className="mvh-drift absolute inset-0"
style={{
animation: "mvh-drift 26s ease-in-out infinite",
backgroundImage:
"radial-gradient(60% 55% at 22% 30%, rgba(16,185,129,0.42) 0%, transparent 62%), radial-gradient(52% 48% at 78% 22%, rgba(56,189,248,0.34) 0%, transparent 60%), radial-gradient(70% 62% at 62% 84%, rgba(99,102,241,0.4) 0%, transparent 65%), radial-gradient(40% 40% at 12% 80%, rgba(244,63,94,0.24) 0%, transparent 60%)",
}}
/>
<div
className="mvh-flicker absolute inset-0 mix-blend-overlay"
style={{
animation: "mvh-flicker 5.5s steps(3, end) infinite",
backgroundImage:
"repeating-linear-gradient(0deg, rgba(255,255,255,0.09) 0px, rgba(255,255,255,0.09) 1px, transparent 1px, transparent 3px)",
}}
/>
<div
className="mvh-scan absolute inset-x-0 top-0 h-32"
style={{
animation: "mvh-scan 7.5s linear infinite",
backgroundImage:
"linear-gradient(to bottom, transparent, rgba(255,255,255,0.14), transparent)",
}}
/>
<div className="absolute inset-0 bg-[radial-gradient(120%_100%_at_50%_0%,transparent_35%,rgba(2,6,23,0.85)_100%)]" />
<div className="absolute inset-0 bg-slate-950/45" />
</div>
<div className="relative mx-auto w-full max-w-6xl px-6 lg:px-8">
<div className="flex flex-col gap-12 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-2xl">
<div className="inline-flex items-center gap-2.5 rounded-full border border-white/15 bg-white/5 px-3.5 py-1.5 text-xs font-medium tracking-wide text-slate-200 backdrop-blur">
<span className="relative flex h-2 w-2">
<span
className="mvh-ring absolute inline-flex h-full w-full rounded-full bg-emerald-400"
style={{ animation: "mvh-pulse-ring 2.4s ease-out infinite" }}
/>
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-400" />
</span>
Field film 04 · Reykjanes geothermal
</div>
<motion.h1
initial={reduceMotion ? false : { opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="mt-6 text-4xl font-semibold tracking-tight text-white sm:text-5xl lg:text-6xl"
>
Sixty-one megawatts,
<span className="block bg-gradient-to-r from-emerald-300 via-sky-300 to-indigo-300 bg-clip-text text-transparent">
and nobody in the control room.
</span>
</motion.h1>
<motion.p
initial={reduceMotion ? false : { opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.08, ease: [0.16, 1, 0.3, 1] }}
className="mt-6 text-base leading-relaxed text-slate-300 sm:text-lg"
>
We spent eleven days on the Reykjanes peninsula filming a plant that
runs itself. No narration, no re-shoots — just the turbines, the
telemetry, and the four people who keep it honest.
</motion.p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<button
type="button"
onClick={togglePlay}
aria-pressed={playing}
className="group inline-flex items-center gap-2.5 rounded-full bg-white px-5 py-3 text-sm font-semibold text-slate-950 transition hover:bg-slate-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
>
{playing ? (
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<rect x="3" y="2" width="3.5" height="12" rx="1" />
<rect x="9.5" y="2" width="3.5" height="12" rx="1" />
</svg>
) : (
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<path d="M4 2.8c0-.8.9-1.3 1.6-.9l7.3 4.4c.7.4.7 1.4 0 1.8l-7.3 4.4c-.7.4-1.6-.1-1.6-.9V2.8Z" />
</svg>
)}
{playing ? "Pause the film" : "Play the film"}
<span className="text-xs font-normal text-slate-500">3:34</span>
</button>
<a
href="#mvh-transcript"
className="inline-flex items-center gap-2 rounded-full border border-white/20 px-5 py-3 text-sm font-medium text-slate-200 transition hover:border-white/40 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
>
Read the transcript
<svg viewBox="0 0 16 16" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="1.7" aria-hidden="true">
<path d="M3 8h10M9 4l4 4-4 4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</a>
</div>
</div>
<dl className="grid shrink-0 grid-cols-3 gap-x-8 gap-y-1 lg:mb-2 lg:grid-cols-1 lg:gap-y-5 lg:border-l lg:border-white/10 lg:pl-8">
{[
{ v: "61 MW", k: "Steady output" },
{ v: "11 days", k: "On location" },
{ v: "4 crew", k: "On the ground" },
].map((stat) => (
<div key={stat.k}>
<dt className="text-xs uppercase tracking-widest text-slate-400">{stat.k}</dt>
<dd className="text-xl font-semibold text-white lg:text-2xl">{stat.v}</dd>
</div>
))}
</dl>
</div>
{/* Player chrome */}
<div className="mt-14 rounded-2xl border border-white/10 bg-white/5 p-4 backdrop-blur-md sm:p-5">
<div className="flex items-center gap-4">
<button
type="button"
onClick={togglePlay}
aria-pressed={playing}
aria-label={playing ? "Pause playback" : "Start playback"}
className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
>
{playing ? (
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<rect x="3" y="2" width="3.5" height="12" rx="1" />
<rect x="9.5" y="2" width="3.5" height="12" rx="1" />
</svg>
) : (
<svg viewBox="0 0 16 16" className="h-4 w-4 translate-x-px" fill="currentColor" aria-hidden="true">
<path d="M4 2.8c0-.8.9-1.3 1.6-.9l7.3 4.4c.7.4.7 1.4 0 1.8l-7.3 4.4c-.7.4-1.6-.1-1.6-.9V2.8Z" />
</svg>
)}
</button>
<span className="w-11 shrink-0 font-mono text-xs tabular-nums text-slate-300">
{formatTime(time)}
</span>
<div className="relative flex-1">
<div className="pointer-events-none absolute inset-x-0 top-1/2 h-1.5 -translate-y-1/2 overflow-hidden rounded-full bg-white/15">
<div
className="h-full rounded-full bg-gradient-to-r from-emerald-400 to-sky-400"
style={{ width: `${progress}%` }}
/>
</div>
{CHAPTERS.slice(1).map((chapter) => (
<span
key={chapter.id}
aria-hidden="true"
className="pointer-events-none absolute top-1/2 h-3 w-px -translate-y-1/2 bg-slate-950/60"
style={{ left: `${(chapter.start / DURATION) * 100}%` }}
/>
))}
<input
type="range"
min={0}
max={DURATION}
step={1}
value={Math.floor(time)}
onChange={(event) => setTime(Number(event.target.value))}
onPointerDown={() => setScrubbing(true)}
onPointerUp={() => setScrubbing(false)}
onKeyDown={() => setScrubbing(true)}
onKeyUp={() => setScrubbing(false)}
onBlur={() => setScrubbing(false)}
aria-label="Seek through the film"
aria-valuetext={`${formatTime(time)} of ${formatTime(DURATION)} — ${activeChapter.label}`}
className="relative h-6 w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none [&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-white [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&:focus-visible::-moz-range-thumb]:ring-2 [&:focus-visible::-moz-range-thumb]:ring-emerald-300 [&:focus-visible::-webkit-slider-thumb]:ring-2 [&:focus-visible::-webkit-slider-thumb]:ring-emerald-300"
/>
</div>
<span className="w-11 shrink-0 text-right font-mono text-xs tabular-nums text-slate-400">
{formatTime(DURATION)}
</span>
<button
type="button"
onClick={() => setMuted((prev) => !prev)}
aria-pressed={!muted}
aria-label={muted ? "Unmute audio" : "Mute audio"}
className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-slate-300 transition hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
>
<svg viewBox="0 0 20 20" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
<path d="M4 7.5h2.5L10 4.5v11L6.5 12.5H4a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1Z" strokeLinejoin="round" />
{muted ? (
<path d="M13.5 7.5l4 5M17.5 7.5l-4 5" strokeLinecap="round" />
) : (
<>
<path d="M13 7.5a3.5 3.5 0 0 1 0 5" strokeLinecap="round" />
<path d="M15.5 5.5a6.5 6.5 0 0 1 0 9" strokeLinecap="round" />
</>
)}
</svg>
</button>
</div>
{/* Chapter rail */}
<div className="mt-5 border-t border-white/10 pt-4">
<div role="group" aria-label="Film chapters" className="flex flex-wrap gap-2">
{CHAPTERS.map((chapter) => {
const isActive = chapter.id === activeChapter.id;
return (
<button
key={chapter.id}
type="button"
aria-current={isActive ? "true" : undefined}
aria-describedby={captionId}
onClick={() => jumpTo(chapter.start)}
className={`inline-flex items-center gap-2 rounded-lg px-3 py-2 text-xs font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 ${
isActive
? "bg-white text-slate-950"
: "text-slate-300 hover:bg-white/10 hover:text-white"
}`}
>
<span className="font-mono tabular-nums text-slate-500">
{formatTime(chapter.start)}
</span>
{chapter.label}
</button>
);
})}
</div>
<p
id={captionId}
className="mt-4 min-h-[2.5rem] text-sm leading-relaxed text-slate-300"
>
{activeChapter.blurb}
</p>
</div>
<p id={statusId} role="status" aria-live="polite" className="sr-only">
{playing
? `Playing — ${activeChapter.label} at ${formatTime(time)}`
: `Paused at ${formatTime(time)} — ${activeChapter.label}`}
</p>
</div>
<p id="mvh-transcript" className="mt-6 text-xs text-slate-500">
Simulated playback for layout preview. Audio is {muted ? "muted" : "on"}.
</p>
</div>
</section>
);
}Dependencies
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 quoteSimilar components
Browse all →
Media Video Player
Originalcustom video player controls UI

Media Audio Player
Originalaudio player with progress and volume

Media Podcast
Originalpodcast episode player card

Media Playlist
Originalmusic playlist with now-playing

Media Mini Player
Originalcompact sticky mini player

Media Video Card
Originalvideo thumbnail card with play overlay

Media Waveform
Originalaudio waveform scrubber

Media Gallery Player
Originalmedia gallery with a lightbox player

Media Live Badge
Originallive stream card with viewer count

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

