"use client" import { useEffect, useRef } from "react" // import "@lottiefiles/dotlottie-wc" // Removed static import to fix SSR error const STOP_DELAY = 700 const LADDER_OFFSET_BOTTOM = 80 const LADDER_OFFSET_TOP = 20 // Buffer to keep character from going off-screen at the top export default function LadderClimb() { const charRef = useRef(null) const ladderRef = useRef(null) const idleRef = useRef(null) const climbRef = useRef(null) const jumpRef = useRef(null) const ladderMaxTravel = useRef(0) const activeState = useRef<"idle" | "climbing" | "jumping">("idle") const scrollTimeout = useRef(null) const isFalling = useRef(false) const switchAnimation = (newState: "idle" | "climbing" | "jumping") => { if (activeState.current === newState) return activeState.current = newState if (!idleRef.current || !climbRef.current || !jumpRef.current) return idleRef.current.style.display = "none" climbRef.current.style.display = "none" jumpRef.current.style.display = "none" if (newState === "idle") idleRef.current.style.display = "block" else if (newState === "climbing") climbRef.current.style.display = "block" else if (newState === "jumping") jumpRef.current.style.display = "block" } const triggerFall = () => { if (!charRef.current || isFalling.current) return const style = window.getComputedStyle(charRef.current) const matrix = new DOMMatrix(style.transform) const currentVisualY = Math.abs(matrix.m42) if (currentVisualY < 5) { switchAnimation("idle") return } isFalling.current = true switchAnimation("jumping") charRef.current.classList.add("falling") charRef.current.style.transform = `translateY(0px)` const handleLanding = (e: TransitionEvent) => { if (e.propertyName !== "transform") return isFalling.current = false charRef.current?.classList.remove("falling") // Force idle animation upon landing switchAnimation("idle") charRef.current?.removeEventListener("transitionend", handleLanding) } charRef.current.addEventListener("transitionend", handleLanding) } const updateDimensions = () => { if (ladderRef.current) { // Logic: The character can only travel the height of the ladder minus the offsets ladderMaxTravel.current = ladderRef.current.clientHeight - LADDER_OFFSET_BOTTOM - LADDER_OFFSET_TOP } } useEffect(() => { import("@lottiefiles/dotlottie-wc") updateDimensions() window.addEventListener("resize", updateDimensions) const handleScroll = () => { if (!charRef.current) return const scrollTop = window.scrollY const docHeight = document.body.scrollHeight - window.innerHeight let progress = docHeight > 0 ? scrollTop / docHeight : 0 // Clamp progress between 0 and 1 progress = Math.max(0, Math.min(1, progress)) // Calculate Target Y based on the ladder's actual height constraint const targetY = progress * ladderMaxTravel.current // Interrupt fall if user starts scrolling if (isFalling.current) { isFalling.current = false charRef.current.classList.remove("falling") } // Apply transformation (constrained to the top of the ladder) charRef.current.style.transform = `translateY(-${targetY}px)` // Set climbing animation if (activeState.current !== "climbing") switchAnimation("climbing") // Stop detection if (scrollTimeout.current) clearTimeout(scrollTimeout.current) scrollTimeout.current = setTimeout(() => { if (targetY > 2) { triggerFall() } else { switchAnimation("idle") } }, STOP_DELAY) } window.addEventListener("scroll", handleScroll, { passive: true }) handleScroll() return () => { window.removeEventListener("scroll", handleScroll) window.removeEventListener("resize", updateDimensions) if (scrollTimeout.current) clearTimeout(scrollTimeout.current) } }, []) return ( <>
{/* The Visual Ladder */}
{/* The Character Container */}
) }