"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { motion, useScroll, useTransform, useSpring, useMotionValueEvent } from "framer-motion"; export default function VideoScrollAnimation() { const containerRef = useRef(null); const videoRef = useRef(null); const [isLoaded, setIsLoaded] = useState(false); const [loadProgress, setLoadProgress] = useState(0); const [videoDuration, setVideoDuration] = useState(0); const { scrollYProgress } = useScroll({ target: containerRef, offset: ["start start", "end end"], }); const smoothProgress = useSpring(scrollYProgress, { stiffness: 100, damping: 30, restDelta: 0.001, }); // Text opacities - simple bottom-positioned captions const introOpacity = useTransform(smoothProgress, [0, 0.12, 0.18, 0.22], [1, 1, 0, 0]); const craftOpacity = useTransform(smoothProgress, [0.28, 0.32, 0.48, 0.52], [0, 1, 1, 0]); const skillsOpacity = useTransform(smoothProgress, [0.55, 0.58, 0.72, 0.75], [0, 1, 1, 0]); const ctaOpacity = useTransform(smoothProgress, [0.85, 0.90, 1, 1], [0, 1, 1, 1]); // Seek video based on scroll progress const seekVideo = useCallback( (progress: number) => { const video = videoRef.current; if (!video || !isLoaded || videoDuration === 0) return; const targetTime = progress * videoDuration; // Only seek if the difference is significant (performance optimization) if (Math.abs(video.currentTime - targetTime) > 0.01) { video.currentTime = targetTime; } }, [isLoaded, videoDuration] ); useMotionValueEvent(smoothProgress, "change", (latest) => { seekVideo(latest); }); // Handle video loading useEffect(() => { const video = videoRef.current; if (!video) return; const handleLoadedMetadata = () => { setVideoDuration(video.duration); }; const handleCanPlayThrough = () => { setIsLoaded(true); // Start at frame 0 video.currentTime = 0; }; const handleProgress = () => { if (video.buffered.length > 0) { const bufferedEnd = video.buffered.end(video.buffered.length - 1); const duration = video.duration; if (duration > 0) { setLoadProgress(Math.round((bufferedEnd / duration) * 100)); } } }; video.addEventListener("loadedmetadata", handleLoadedMetadata); video.addEventListener("canplaythrough", handleCanPlayThrough); video.addEventListener("progress", handleProgress); // Force load video.load(); return () => { video.removeEventListener("loadedmetadata", handleLoadedMetadata); video.removeEventListener("canplaythrough", handleCanPlayThrough); video.removeEventListener("progress", handleProgress); }; }, []); // Initial seek after load useEffect(() => { if (isLoaded) { seekVideo(smoothProgress.get()); } }, [isLoaded, seekVideo, smoothProgress]); return ( <> {/* Loading Screen */} {!isLoaded && ( {loadProgress}%
)}
{/* Sticky video container */}
); }