// ───────────────────────────────────────────────────────────── // MOMENTUM CHIP ROW — Carrossel horizontal de chips de categoria. // Mesma física do carrossel dos cartazes (arrasto + inércia + // fricção), só que SEM auto-rotação. Tap = filtra a categoria. // ───────────────────────────────────────────────────────────── const { useRef: __useRef, useEffect: __useEffect } = React; function MomentumChipRow({ items, active, onPick, t }) { const wrapRef = __useRef(null); const trackRef = __useRef(null); const offsetRef = __useRef(0); const velocityRef = __useRef(0); const rafRef = __useRef(0); const lastTimeRef = __useRef(0); const draggingRef = __useRef(false); const startXRef = __useRef(0); const startOffsetRef = __useRef(0); const lastPxRef = __useRef(0); const lastTRef = __useRef(0); const movedRef = __useRef(false); const maxScrollRef = __useRef(0); const FRICTION = 0.93; // mesmo do carrossel const DRAG_THRESHOLD = 6; // Atualiza o limite máximo de scroll após render __useEffect(() => { const wrap = wrapRef.current, track = trackRef.current; if (!wrap || !track) return; const recalc = () => { const max = Math.max(0, track.scrollWidth - wrap.clientWidth); maxScrollRef.current = max; }; recalc(); const ro = new ResizeObserver(recalc); ro.observe(track); return () => ro.disconnect(); }, [items.length]); // Loop de física (só roda enquanto houver velocidade) __useEffect(() => { let canceled = false; const loop = (now) => { if (canceled) return; const dt = Math.min(50, now - (lastTimeRef.current || now)); lastTimeRef.current = now; if (!draggingRef.current && Math.abs(velocityRef.current) > 0.02) { offsetRef.current += velocityRef.current * dt; velocityRef.current *= FRICTION; // Clamp nas bordas com leve "bounce" elastico const max = maxScrollRef.current; if (offsetRef.current > 0) { offsetRef.current = 0; velocityRef.current = 0; } else if (offsetRef.current < -max) { offsetRef.current = -max; velocityRef.current = 0; } if (trackRef.current) { trackRef.current.style.transform = `translate3d(${offsetRef.current}px,0,0)`; } } rafRef.current = requestAnimationFrame(loop); }; rafRef.current = requestAnimationFrame(loop); return () => { canceled = true; cancelAnimationFrame(rafRef.current); }; }, []); __useEffect(() => { const wrap = wrapRef.current; if (!wrap) return; const onDown = (e) => { if (e.pointerType === "mouse" && e.button !== 0) return; draggingRef.current = true; movedRef.current = false; startXRef.current = e.clientX; startOffsetRef.current = offsetRef.current; lastPxRef.current = e.clientX; lastTRef.current = performance.now(); velocityRef.current = 0; if (wrap.style) wrap.style.cursor = "grabbing"; }; const onMove = (e) => { if (!draggingRef.current) return; const dx = e.clientX - startXRef.current; if (Math.abs(dx) > DRAG_THRESHOLD) { movedRef.current = true; e.preventDefault?.(); } if (movedRef.current) { // Aplica com clamp const max = maxScrollRef.current; let next = startOffsetRef.current + dx; if (next > 0) next = 0 - (next * -0.3); // resistência elástica else if (next < -max) next = -max + ((next + max) * 0.3); offsetRef.current = next; if (trackRef.current) { trackRef.current.style.transform = `translate3d(${next}px,0,0)`; } const now = performance.now(); const ddt = now - lastTRef.current; if (ddt > 0) { const dxLocal = e.clientX - lastPxRef.current; velocityRef.current = dxLocal / ddt; } lastPxRef.current = e.clientX; lastTRef.current = now; } }; const onUp = () => { draggingRef.current = false; if (wrap.style) wrap.style.cursor = "grab"; // Snap-back se passou da borda const max = maxScrollRef.current; if (offsetRef.current > 0) { velocityRef.current = -offsetRef.current * 0.3; } else if (offsetRef.current < -max) { velocityRef.current = (-max - offsetRef.current) * -0.3; } }; // Wheel horizontal (scroll do trackpad vertical vira horizontal) const onWheel = (e) => { if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { velocityRef.current = -e.deltaY * 0.4; e.preventDefault(); } }; wrap.addEventListener("pointerdown", onDown); window.addEventListener("pointermove", onMove, { passive: false }); window.addEventListener("pointerup", onUp); window.addEventListener("pointercancel", onUp); wrap.addEventListener("wheel", onWheel, { passive: false }); return () => { wrap.removeEventListener("pointerdown", onDown); window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); window.removeEventListener("pointercancel", onUp); wrap.removeEventListener("wheel", onWheel); }; }, []); const onClickCapture = (e) => { if (movedRef.current) { e.stopPropagation(); e.preventDefault(); movedRef.current = false; } }; return (
{items.map((c) => ( ))}
); } Object.assign(window, { MomentumChipRow });