export class CounterAnimation { static DURATION_MS = 2000; init() { const counters = document.querySelectorAll('.counter-value[data-target]'); if (counters.length === 0) return; if ('IntersectionObserver' in window) { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { this._animate(entry.target); observer.unobserve(entry.target); } }); }, { threshold: 0.2 }); counters.forEach(c => observer.observe(c)); } else { counters.forEach(c => this._animate(c)); } } _animate(el) { const target = Number.parseInt((el.dataset.target || '0').replace(/[^0-9]/g, ''), 10); if (Number.isNaN(target)) return; CounterAnimation.animateValue(el, target, el.dataset.suffix || ''); } // Reusable easing tween from 0 → target. Callable for stats whose // value is only known after an async fetch (no data-target needed). static animateValue(el, target, suffix = '') { if (!el || Number.isNaN(target)) return; const startTime = performance.now(); const tick = (now) => { const progress = Math.min((now - startTime) / CounterAnimation.DURATION_MS, 1); const eased = 1 - Math.pow(1 - progress, 3); el.textContent = Math.floor(eased * target) + suffix; if (progress < 1) requestAnimationFrame(tick); else el.textContent = target + suffix; }; requestAnimationFrame(tick); } }