import { createContext, RefObject, useContext, useEffect, useRef, useState, } from "react" import { motion, MotionValue, useMotionValue, useScroll, UseScrollOptions, useSpring, useTime, useTransform, } from "motion/react" import { cn } from "@/lib/utils" type PreserveAspectRatioAlign = | "none" | "xMinYMin" | "xMidYMin" | "xMaxYMin" | "xMinYMid" | "xMidYMid" | "xMaxYMid" | "xMinYMax" | "xMidYMax" | "xMaxYMax" type PreserveAspectRatioMeetOrSlice = "meet" | "slice" type PreserveAspectRatio = | PreserveAspectRatioAlign | `${Exclude} ${PreserveAspectRatioMeetOrSlice}` interface ElementAlongPathProps { // Path properties path: string pathId?: string className?: string preserveAspectRatio?: PreserveAspectRatio showPath?: boolean direction?: "normal" | "reverse" // SVG properties width?: string | number height?: string | number viewBox?: string // Animation properties animationType?: "auto" | "scroll" // Animation properties if animationType is auto duration?: number transition?: any // Scroll animation properties if animationType is scroll scrollContainer?: RefObject scrollOffset?: UseScrollOptions["offset"] scrollTransformValues?: [number, number] // Children children?: React.ReactNode } interface ElementAlongPathItemProps { children: React.ReactNode className?: string startOffset?: number // 0-100 percentage transition?: any // Override parent transition } // Create context const ElementAlongPathContext = createContext<{ path: string animationType: "auto" | "scroll" direction: "normal" | "reverse" // Add direction to context progress: MotionValue scrollYProgress: MotionValue scrollTransformValues: [number, number] transition: any setHovered: (isHovered: boolean) => void } | null>(null) // Context hook export const useElementAlongPathContext = () => { const context = useContext(ElementAlongPathContext) if (!context) { throw new Error("ElementAlongPathItem must be used within ElementAlongPath") } return context } // Item component export const ElementAlongPathItem = ({ children, className, startOffset = 0, transition: itemTransition, }: ElementAlongPathItemProps) => { const { path, animationType, progress, scrollYProgress, scrollTransformValues, direction, transition: parentTransition, setHovered, } = useElementAlongPathContext() // Use item transition if provided, otherwise use parent transition const transition = itemTransition || parentTransition const initialOffset = direction === "normal" ? `${startOffset}%` : `${100 - startOffset}%` const animateOffset = direction === "normal" ? "100%" : "0%" const scp = useTransform( scrollYProgress, [0, 1], [scrollTransformValues[0], scrollTransformValues[1]] ) return ( setHovered(true)} // onHoverEnd={() => setHovered(false)} > {children} ) } const ElementAlongPath = ({ children, // Path defaults path, pathId, preserveAspectRatio = "xMidYMid meet", showPath = false, className, // SVG defaults width = "100%", height = "100%", viewBox = "0 0 100 100", // Animation type animationType = "auto", direction = "normal", // Animation defaults duration = 4, transition = { duration: 4, repeat: Infinity, ease: "linear" }, // Scroll animation defaults scrollContainer, scrollOffset = ["start end", "end end"], scrollTransformValues = [0, 100], }: ElementAlongPathProps) => { const container = useRef(null) const [isHovered, setIsHovered] = useState(false) // Create a time scale factor that changes based on hover state const timeScale = useMotionValue(1) // Update time scale when hover state changes useEffect(() => { timeScale.set(isHovered ? 0.3 : 1) // Slow down to 30% speed when hovered }, [isHovered, timeScale]) // Create a spring-based time scale for smooth transitions const smoothTimeScale = useSpring(timeScale, { stiffness: 100, damping: 30, }) const t = useTime() const scaledTime = useTransform(t, (time) => time * smoothTimeScale.get()) const progress = useTransform( scaledTime, [0, duration], direction === "normal" ? [0, 100] : [100, 0] ) // naive id for the path. you should rather use yours :) const id = pathId || `animated-path-${Math.random().toString(36).substring(7)}` const { scrollYProgress } = useScroll({ container: scrollContainer || container, offset: scrollOffset, }) // Adjust scroll progress based on direction const scrollProgressValues = direction === "normal" ? [scrollTransformValues[0], scrollTransformValues[1]] : [scrollTransformValues[1], scrollTransformValues[0]] const scrollProgress = useTransform( scrollYProgress, [0, 1], scrollProgressValues ) // Create the progress value based on animation type const finalProgress = animationType === "auto" ? progress : scrollProgress return (
{children}
) } export default ElementAlongPath