feat(projects): video de rolagem na capa dos cards, carregado so quando o card aparece

video autoplay muted loop playsinline preload=none com poster; as fontes entram
via IntersectionObserver e o video pausa fora da tela. Com prefers-reduced-motion
aparece so o poster.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
Evandro Fonseca JuniorandClaude Sonnet 5.5 committed 2026-10-08 11:33:17 -03:00
1 parent ca9c0604b8
commit a67892613b
6 files changed
+276 -2

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
"use client";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import type { ProjectVideoAsset } from "@/lib/project-videos";
/**
* Vídeo de rolagem do projeto na capa do card. Nada de vídeo é baixado até o card entrar na tela
* (IntersectionObserver); fora da tela ele pausa. Com prefers-reduced-motion só o poster aparece.
*/
export function ProjectVideo({
video,
title,
}: {
video: ProjectVideoAsset;
title: string;
}) {
const reducedMotion = useReducedMotion();
const ref = useRef<HTMLVideoElement>(null);
const [activated, setActivated] = useState(false);
useEffect(() => {
const element = ref.current;
if (reducedMotion || !element) return;
if (typeof IntersectionObserver === "undefined") {
setActivated(true);
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setActivated(true);
element.play().catch(() => {});
} else {
element.pause();
}
},
{ rootMargin: "120px 0px", threshold: 0.2 },
);
observer.observe(element);
return () => observer.disconnect();
}, [reducedMotion]);
useEffect(() => {
const element = ref.current;
if (!activated || reducedMotion || !element) return;
element.muted = true; // o React não reflete `muted` no DOM e o autoplay exige o valor real
element.load();
element.play().catch(() => {});
}, [activated, reducedMotion]);
if (reducedMotion) {
return (
<Image
src={video.poster}
alt={`Captura de tela de ${title}`}
width={video.width}
height={video.height}
sizes="(min-width: 1360px) 33vw, (min-width: 1025px) 50vw, 100vw"
/>
);
}
return (
<video
ref={ref}
autoPlay
muted
loop
playsInline
preload="none"
poster={video.poster}
width={video.width}
height={video.height}
aria-label={`Demonstração em vídeo de ${title}`}
disablePictureInPicture
disableRemotePlayback
>
{activated &&
video.sources.map((source) => (
<source key={source.src} src={source.src} type={source.type} />
))}
</video>
);
}