import { Controller } from "@hotwired/stimulus" // Counter controller for animating number increments // Used for statistics and numerical displays that animate when they come into view export default class extends Controller { // Define controller values with defaults static values = { target: { type: Number, default: 0 }, // Target number to count to decimal: { type: Boolean, default: false }, // Whether to display decimal values duration: { type: Number, default: 2000 } // Animation duration in milliseconds } // Set up the intersection observer when the controller connects connect() { // Create an intersection observer to trigger animation when element is visible this.observer = new IntersectionObserver((entries) => { entries.forEach(entry => { // Start animation when element is 50% visible if (entry.isIntersecting) { this.animate() // Stop observing after animation starts this.observer.unobserve(this.element) } }) }, { threshold: 0.5 }) // Begin observing this element this.observer.observe(this.element) } // Clean up the observer when the controller disconnects disconnect() { if (this.observer) { this.observer.disconnect() } } // Animate the counter from 0 to the target value animate() { // Find the target element with data-target-value const targetElement = this.element.querySelector('.stat-number'); if (!targetElement) return; // Get the target value this.targetValue = parseInt(targetElement.getAttribute('data-target-value'), 10) || this.targetValue; const startValue = 0; const startTime = performance.now(); // Update counter function using requestAnimationFrame for smooth animation const updateCounter = (currentTime) => { const elapsedTime = currentTime - startTime; const progress = Math.min(elapsedTime / this.durationValue, 1); // Easing function for smooth animation (ease-out quartic) const easeOutQuart = 1 - Math.pow(1 - progress, 4); let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart; // Format value based on decimal setting if (this.decimalValue && this.targetValue < 10) { currentValue = currentValue.toFixed(1); } else { currentValue = Math.floor(currentValue); } // Update only the text content of the target element targetElement.textContent = currentValue; // Continue animation until complete if (progress < 1) { requestAnimationFrame(updateCounter); } else { // Ensure final value is exactly the target const finalValue = this.decimalValue && this.targetValue < 10 ? this.targetValue.toFixed(1) : this.targetValue; targetElement.textContent = finalValue; } } // Start the animation requestAnimationFrame(updateCounter); } }