UNPKG

lightswind

Version:

A collection of beautifully crafted React Components, Blocks & Templates for Modern Developers. Create stunning web applications effortlessly by using our 160+ professional and animated react components.

125 lines (124 loc) 6.13 kB
// @ts-nocheck "use client"; import { jsx as _jsx } from "react/jsx-runtime"; import { motion, useAnimationFrame, useInView, useMotionValue, useScroll, useSpring, useTransform, useVelocity, } from "framer-motion"; import React, { useContext, useEffect, useRef, useState } from "react"; // Assuming you have a utility function for Tailwind CSS class merging const cn = (...classes) => classes.filter(Boolean).join(' '); /** * Utility function to wrap a value within a min/max range for continuous looping. * @param min The minimum value of the range. * @param max The maximum value of the range. * @param v The value to wrap. */ export const wrap = (min, max, v) => { const rangeSize = max - min; return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min; }; // --- Context for Shared Scroll Velocity --- const ScrollVelocityContext = React.createContext(null); // --- ScrollVelocityContainer (Parent Component) --- export function ScrollVelocityContainer({ children, className, ...props }) { const { scrollY } = useScroll(); const scrollVelocity = useVelocity(scrollY); const smoothVelocity = useSpring(scrollVelocity, { damping: 50, stiffness: 400, }); // Transforms the smooth scroll velocity into a factor used to control the marquee speed. const velocityFactor = useTransform(smoothVelocity, (v) => { const sign = v < 0 ? -1 : 1; // Clamps the magnitude to a max of 5 for reasonable speed const magnitude = Math.min(5, (Math.abs(v) / 1000) * 5); return sign * magnitude; }); return (_jsx(ScrollVelocityContext.Provider, { value: velocityFactor, children: _jsx("div", { className: cn("relative w-full", className), ...props, children: children }) })); } // --- ScrollVelocityRow (Router) --- // Decides whether to use shared context or local velocity calculation. export function ScrollVelocityRow(props) { const sharedVelocityFactor = useContext(ScrollVelocityContext); if (sharedVelocityFactor) { return (_jsx(ScrollVelocityRowImpl, { ...props, velocityFactor: sharedVelocityFactor })); } return _jsx(ScrollVelocityRowLocal, { ...props }); } function ScrollVelocityRowImpl({ children, baseVelocity = 5, direction = 1, className, velocityFactor, ...props }) { const containerRef = useRef(null); const [numCopies, setNumCopies] = useState(1); const x = useMotionValue(0); // State added to handle pause on hover const [isHovered, setIsHovered] = useState(false); const prevTimeRef = useRef(0); const unitWidthRef = useRef(0); const baseXRef = useRef(0); // Optimize: Check if container is in view const isInView = useInView(containerRef, { margin: "20%" }); // ResizeObserver to determine how many copies are needed for continuous loop useEffect(() => { const container = containerRef.current; if (!container) return; const ro = new ResizeObserver(([entry]) => { const containerWidth = entry.contentRect.width; const block = container.querySelector(".scroll-velocity-block"); if (!block) return; const blockWidth = block.scrollWidth; unitWidthRef.current = blockWidth; if (blockWidth > 0) { // Calculate copies: need at least 3, or enough to cover the screen plus padding const nextCopies = Math.max(3, Math.ceil(containerWidth / blockWidth) + 2); setNumCopies(nextCopies); } }); ro.observe(container); return () => ro.disconnect(); }, []); // Frame-by-frame animation logic useAnimationFrame((time) => { if (!isInView || isHovered) { prevTimeRef.current = time; return; } const dt = (time - prevTimeRef.current) / 1000; prevTimeRef.current = time; const unitWidth = unitWidthRef.current; if (unitWidth <= 0) return; const velocity = velocityFactor.get(); const speedMultiplier = Math.min(5, Math.abs(velocity)); const scrollDirection = velocity >= 0 ? 1 : -1; const currentDirection = direction * scrollDirection; // Calculate how far to move this frame const pixelsPerSecond = (unitWidth * baseVelocity) / 100; const moveBy = currentDirection * pixelsPerSecond * (1 + speedMultiplier) * dt; // Update position and apply the wrap function for seamless looping const newX = baseXRef.current + moveBy; baseXRef.current = wrap(0, unitWidth, newX); x.set(baseXRef.current); }); const childrenArray = React.Children.toArray(children); return (_jsx("div", { ref: containerRef, className: cn("w-full overflow-hidden whitespace-nowrap", className), ...props, // Handlers to control the hover state onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: _jsx(motion.div, { className: "inline-flex will-change-transform transform-gpu", // Use useTransform to invert the x value for a left-to-right scroll effect style: { x: useTransform(x, (v) => `${-v}px`) }, children: Array.from({ length: numCopies }).map((_, i) => (_jsx("div", { className: cn("inline-flex shrink-0", // Mark the first copy to measure its width for looping i === 0 && "scroll-velocity-block"), "aria-hidden": i !== 0, children: childrenArray }, i))) }) })); } // --- ScrollVelocityRowLocal (Fallback for standalone usage) --- function ScrollVelocityRowLocal(props) { const { scrollY } = useScroll(); const localVelocity = useVelocity(scrollY); const localSmoothVelocity = useSpring(localVelocity, { damping: 50, stiffness: 400, }); const localVelocityFactor = useTransform(localSmoothVelocity, (v) => { const sign = v < 0 ? -1 : 1; const magnitude = Math.min(5, (Math.abs(v) / 1000) * 5); return sign * magnitude; }); return (_jsx(ScrollVelocityRowImpl, { ...props, velocityFactor: localVelocityFactor })); }