Web InnoventixFreeCode

Media Video Hero

Original · free

hero with a video-style backdrop

byWeb InnoventixReact + Tailwind
mediavideohero
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-hero.json
media-video-hero.tsx
"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 &middot; 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 &mdash; 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

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 →