Componentes
[ui] · motion

ScrollBasedVelocity

Marquee infinito cuya velocidad y dirección reaccionan al scroll del usuario.

motion
@/components/ui/scroll-based-velocity
SFESS UI✦
1"use client";
2import React, { useContext, useEffect, useRef, useState } from "react"
3import {
4 motion,
5 useAnimationFrame,
6 useMotionValue,
7 useScroll,
8 useSpring,
9 useTransform,
10 useVelocity,
11} from "motion/react"
12 
13import { cn } from "@/lib/utils"
14 
15export const wrap = (min, max, v) => {
16 const rangeSize = max - min
17 return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min
18}
19 
20const ScrollVelocityContext = React.createContext(null)
21 
22export function ScrollVelocityContainer({
23 children,
24 className,
25 ...props
26}) {
27 const { scrollY } = useScroll()
28 const scrollVelocity = useVelocity(scrollY)
29 const smoothVelocity = useSpring(scrollVelocity, {
30 damping: 50,
31 stiffness: 400,
32 })
33 const velocityFactor = useTransform(smoothVelocity, (v) => {
34 const sign = v < 0 ? -1 : 1
35 const magnitude = Math.min(5, (Math.abs(v) / 1000) * 5)
36 return sign * magnitude
37 })
38 
39 return (
40 <ScrollVelocityContext.Provider value={velocityFactor}>
41 <div className={cn("relative w-full", className)} {...props}>
42 {children}
43 </div>
44 </ScrollVelocityContext.Provider>
45 );
46}
47 
48export function ScrollVelocityRow(props) {
49 const sharedVelocityFactor = useContext(ScrollVelocityContext)
50 if (sharedVelocityFactor) {
51 return (<ScrollVelocityRowImpl {...props} velocityFactor={sharedVelocityFactor} />);
52 }
53 return <ScrollVelocityRowLocal {...props} />;
54}
55 
56function ScrollVelocityRowImpl({
57 children,
58 baseVelocity = 5,
59 direction = 1,
60 className,
61 velocityFactor,
62 scrollReactivity = true,
63 ...props
64}) {
65 const containerRef = useRef(null)
66 const blockRef = useRef(null)
67 const [numCopies, setNumCopies] = useState(1)
68 
69 const baseX = useMotionValue(0)
70 const baseDirectionRef = useRef(direction >= 0 ? 1 : -1)
71 const currentDirectionRef = useRef(direction >= 0 ? 1 : -1)
72 const unitWidth = useMotionValue(0)
73 
74 const isInViewRef = useRef(true)
75 const isPageVisibleRef = useRef(true)
76 const prefersReducedMotionRef = useRef(false)
77 
78 useEffect(() => {
79 const container = containerRef.current
80 const block = blockRef.current
81 let ro = null
82 let io = null
83 let mq = null
84 const handleVisibility = () => {
85 isPageVisibleRef.current = document.visibilityState === "visible"
86 }
87 const handlePRM = () => {
88 if (mq) {
89 prefersReducedMotionRef.current = mq.matches
90 }
91 }
92 
93 if (container && block) {
94 const updateSizes = () => {
95 const cw = container.offsetWidth || 0
96 const bw = block.scrollWidth || 0
97 unitWidth.set(bw)
98 const nextCopies = bw > 0 ? Math.max(3, Math.ceil(cw / bw) + 2) : 1
99 setNumCopies((prev) => (prev === nextCopies ? prev : nextCopies))
100 }
101 
102 updateSizes()
103 
104 ro = new ResizeObserver(updateSizes)
105 ro.observe(container)
106 ro.observe(block)
107 
108 io = new IntersectionObserver(([entry]) => {
109 isInViewRef.current = entry.isIntersecting
110 })
111 io.observe(container)
112 
113 document.addEventListener("visibilitychange", handleVisibility, {
114 passive: true,
115 })
116 handleVisibility()
117 
118 mq = window.matchMedia("(prefers-reduced-motion: reduce)")
119 mq.addEventListener("change", handlePRM)
120 handlePRM()
121 }
122 
123 return () => {
124 if (ro) {
125 ro.disconnect()
126 }
127 if (io) {
128 io.disconnect()
129 }
130 document.removeEventListener("visibilitychange", handleVisibility)
131 if (mq) {
132 mq.removeEventListener("change", handlePRM)
133 }
134 };
135 }, [children, unitWidth])
136 
137 const x = useTransform([baseX, unitWidth], ([v, bw]) => {
138 const width = Number(bw) || 1
139 const offset = Number(v) || 0
140 return `${-wrap(0, width, offset)}px`;
141 })
142 
143 useAnimationFrame((_, delta) => {
144 if (!isInViewRef.current || !isPageVisibleRef.current) return
145 const dt = delta / 1000
146 const vf = scrollReactivity ? velocityFactor.get() : 0
147 const absVf = Math.min(5, Math.abs(vf))
148 const speedMultiplier = prefersReducedMotionRef.current ? 1 : 1 + absVf
149 
150 if (absVf > 0.1) {
151 const scrollDirection = vf >= 0 ? 1 : -1
152 currentDirectionRef.current = baseDirectionRef.current * scrollDirection
153 }
154 
155 const bw = unitWidth.get() || 0
156 if (bw <= 0) return
157 const pixelsPerSecond = (bw * baseVelocity) / 100
158 const moveBy =
159 currentDirectionRef.current * pixelsPerSecond * speedMultiplier * dt
160 baseX.set(baseX.get() + moveBy)
161 })
162 
163 return (
164 <div
165 ref={containerRef}
166 className={cn("w-full overflow-hidden whitespace-nowrap", className)}
167 {...props}>
168 <motion.div
169 className="inline-flex transform-gpu items-center will-change-transform select-none"
170 style={{ x }}>
171 {Array.from({ length: numCopies }).map((_, i) => (
172 <div
173 key={i}
174 ref={i === 0 ? blockRef : null}
175 aria-hidden={i !== 0}
176 className="inline-flex shrink-0 items-center">
177 {children}
178 </div>
179 ))}
180 </motion.div>
181 </div>
182 );
183}
184 
185function ScrollVelocityRowLocal(props) {
186 const { scrollY } = useScroll()
187 const localVelocity = useVelocity(scrollY)
188 const localSmoothVelocity = useSpring(localVelocity, {
189 damping: 50,
190 stiffness: 400,
191 })
192 const localVelocityFactor = useTransform(localSmoothVelocity, (v) => {
193 const sign = v < 0 ? -1 : 1
194 const magnitude = Math.min(5, (Math.abs(v) / 1000) * 5)
195 return sign * magnitude
196 })
197 return (<ScrollVelocityRowImpl {...props} velocityFactor={localVelocityFactor} />);
198}
199 

Uso

Props

PropTipoDefaultDescripción
baseVelocitynumber5Velocidad base (% del ancho/s).
direction1 | -11Dirección inicial.
scrollReactivitybooleantrueReacciona a la velocidad del scroll.

Acciones del componente

Search for a command to run...

Potenciá tu negocio con SFESS

Software a medida, IA integrada y ciberseguridad avanzada para empresas.

0años de ingeniería
0proyectos entregados
0% uptime
Servicios destacados
  • Consultoría SEO Avanzado & Analítica

    Auditoría técnica profunda para posicionar tu negocio en los primeros resultados de Google.

    SEO TécnicoGA4
  • Páginas Web Corporativas Complejas

    Plataformas empresariales masivas con múltiples secciones y animaciones fluidas.

    Framer MotionMulti-page
  • Integración de Inteligencia Artificial en Negocios

    Automatizamos la atención, las reservas y las ventas de tu negocio con agentes de IA.

    AI AgentsChatbots
Nuestro ecosistema
WORKENTA_OSSFESS_SOLUTIONSPRANALUZ_YOGAREMY_AI
Ver todos los servicios