Loomia logo.png
Menu
HomePricingBlogWork
Book a 15-min intro callhello@loomia.com
class MenuToggle {
    constructor(config = {}) {
        this.config = {
            toggleSelector: '[data-menu-toggle]',
            overlaySelector: '[data-menu-overlay]',
            openText: 'Close',
            closedText: 'Menu',
            animationDuration: 100,
            staggerDelay: 80,
            ...config
        };
        
        this.isOpen = false;
        this.initialized = false;
        
        this.init();
    }
    
    init() {
        if (this.initialized) return;
        
        this.elements = this.getElements();
        if (!this.elements.toggle || !this.elements.overlay) {
            return;
        }
        
        this.injectStyles();
        this.forceInitialStateImmediate();
        this.createArrow();
        this.bindEvents();
        this.setAriaAttributes();
        this.setupMenuItems();
        
        this.initialized = true;
    }
    
    forceInitialStateImmediate() {
        this.elements.overlay.style.transform = 'translateY(-100%)';
        this.elements.overlay.style.transition = 'none';
        this.elements.overlay.classList.add('menu-closed');
        this.isOpen = false;
        
        setTimeout(() => {
            this.elements.overlay.style.transition = 'transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1)';
        }, 50);
    }
    
    getElements() {
        return {
            toggle: document.querySelector(this.config.toggleSelector),
            overlay: document.querySelector(this.config.overlaySelector)
        };
    }
    
    setupMenuItems() {
        this.menuItems = this.elements.overlay.querySelectorAll('*:not(style):not(script)');
        this.menuItems = Array.from(this.menuItems).filter(item => {
            return item.textContent.trim().length > 0 && 
                   !item.querySelector('*') && 
                   item.offsetParent !== null;
        });
        
        this.menuItems.forEach((item, index) => {
            item.style.opacity = '0';
            item.style.transform = 'translateY(30px)';
            item.style.transition = `opacity 0.5s ease ${index * this.config.staggerDelay}ms, transform 0.5s ease ${index * this.config.staggerDelay}ms`;
        });
    }
    
    injectStyles() {
        if (document.getElementById('menu-toggle-styles')) return;
        
        const style = document.createElement('style');
        style.id = 'menu-toggle-styles';
        style.textContent = `
            ${this.config.overlaySelector} {
                position: fixed !important;
                top: 0 !important;
                left: 0 !important;
                width: 100vw !important;
                height: 100vh !important;
                z-index: 9999 !important;
            }
            
            body.menu-open .brx-header {
                z-index: 1 !important;
            }
            
            .menu-arrow {
                display: inline-block;
                margin-right: 8px;
                transition: transform 0.4s cubic-bezier(0.4, 0.0, 0.2, 1);
                width: 8px;
                height: 8px;
                border-right: 2px solid currentColor;
                border-bottom: 2px solid currentColor;
                transform: rotate(45deg);
                transform-origin: center;
                vertical-align: middle;
                position: relative;
                top: -1px;
            }
            
            .menu-arrow.rotated {
                transform: rotate(-135deg);
            }
            
            body.menu-open {
                overflow: hidden !important;
            }
        `;
        document.head.appendChild(style);
    }
    
    createArrow() {
        this.arrow = document.createElement('span');
        this.arrow.className = 'menu-arrow';
        this.arrow.setAttribute('aria-hidden', 'true');
        
        this.textNode = document.createTextNode(this.config.closedText);
        
        this.elements.toggle.innerHTML = '';
        this.elements.toggle.appendChild(this.arrow);
        this.elements.toggle.appendChild(this.textNode);
    }
    
    setAriaAttributes() {
        this.elements.toggle.setAttribute('aria-expanded', 'false');
        this.elements.toggle.setAttribute('aria-controls', this.elements.overlay.id || 'menu-overlay');
        this.elements.overlay.setAttribute('aria-hidden', 'true');
    }
    
    bindEvents() {
        this.boundClickHandler = this.handleClick.bind(this);
        this.boundKeyHandler = this.handleKeydown.bind(this);
        
        this.elements.toggle.addEventListener('click', this.boundClickHandler);
        this.elements.toggle.addEventListener('keydown', this.boundKeyHandler);
    }
    
    handleKeydown(e) {
        if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            this.toggle();
        }
        if (e.key === 'Escape' && this.isOpen) {
            this.close();
        }
    }
    
    handleClick(e) {
        e.preventDefault();
        this.toggle();
    }
    
    toggle() {
        this.isOpen ? this.close() : this.open();
    }
    
    open() {
        if (this.isOpen) return;
        
        document.body.classList.add('menu-open');
        this.elements.overlay.classList.remove('menu-closed');
        this.elements.overlay.style.transform = 'translateY(0)';
        this.textNode.textContent = this.config.openText;
        this.arrow.classList.add('rotated');
        
        setTimeout(() => {
            this.menuItems.forEach((item, index) => {
                setTimeout(() => {
                    item.style.opacity = '1';
                    item.style.transform = 'translateY(0)';
                }, index * this.config.staggerDelay);
            });
        }, 300);
        
        this.elements.toggle.setAttribute('aria-expanded', 'true');
        this.elements.overlay.setAttribute('aria-hidden', 'false');
        
        this.isOpen = true;
    }
    
    close() {
        if (!this.isOpen) return;
        
        document.body.classList.remove('menu-open');
        
        this.menuItems.forEach((item) => {
            item.style.opacity = '0';
            item.style.transform = 'translateY(30px)';
        });
        
        setTimeout(() => {
            this.elements.overlay.style.transform = 'translateY(-100%)';
            this.elements.overlay.classList.add('menu-closed');
        }, 200);
        
        this.textNode.textContent = this.config.closedText;
        this.arrow.classList.remove('rotated');
        
        this.elements.toggle.setAttribute('aria-expanded', 'false');
        this.elements.overlay.setAttribute('aria-hidden', 'true');
        
        this.isOpen = false;
    }
    
    destroy() {
        if (!this.initialized) return;
        
        this.elements.toggle.removeEventListener('click', this.boundClickHandler);
        this.elements.toggle.removeEventListener('keydown', this.boundKeyHandler);
        
        document.body.classList.remove('menu-open');
        
        const style = document.getElementById('menu-toggle-styles');
        if (style) style.remove();
        
        this.initialized = false;
    }
}

document.addEventListener('DOMContentLoaded', () => {
    new MenuToggle();
});
Pitch Decks
Branding
Web Design
UX Design
Social Graphics
(function(){
  // Inject styles only once globally
  if (!document.getElementById('element-carousel-styles')) {
    const style = document.createElement('style');
    style.id = 'element-carousel-styles';
    style.textContent = `
      .element-carousel-preview {
        display: flex;
        align-items: center;
        overflow: hidden;
        position: relative;
        width: 100%;
        height: 100%;
      }
      
      .element-carousel-track {
        display: flex;
        align-items: center;
        will-change: transform;
        padding: 0;
        position: relative;
        gap: 15px;
        backface-visibility: hidden;
        perspective: 1000px;
        transform: translateZ(0);
        height: 100%;
      }
      
      .element-carousel-track > * {
        flex-shrink: 0;
        transition: all 0.3s ease;
        opacity: 0.95;
      }
      
      .element-carousel-track > *:hover {
        opacity: 1;
        transform: translateY(-2px);
      }
      
      .carousel-blur-left,
      .carousel-blur-right {
        position: absolute;
        top: 0;
        bottom: 0;
        width: min(80px, 15%);
        pointer-events: none;
        z-index: 1;
      }
      
      .carousel-blur-left {
        left: 0;
        background: linear-gradient(to right, rgba(255,255,255,0.9), rgba(255,255,255,0));
      }
      
      .carousel-blur-right {
        right: 0;
        background: linear-gradient(to left, rgba(255,255,255,0.9), rgba(255,255,255,0));
      }
    `;
    document.head.appendChild(style);
  }

  // Centralized Carousel Manager for performance optimization
  class CarouselManager {
    constructor() {
      this.instances = new Map();
      this.animationId = null;
      this.isAnimating = false;
      this.intersectionObserver = null;
      this.resizeObserver = null;
      this.resizeTimer = null;
      this.scrollTimer = null;
      this.isScrolling = false;
      
      this.setupGlobalObservers();
      this.setupGlobalEventListeners();
    }

    setupGlobalObservers() {
      // Single intersection observer for all carousels
      this.intersectionObserver = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          const carousel = this.instances.get(entry.target);
          if (carousel) {
            carousel.isVisible = entry.isIntersecting;
            this.updateAnimationState();
          }
        });
      }, {
        threshold: 0.1,
        rootMargin: '50px'
      });

      // Single resize observer for all carousels
      if (window.ResizeObserver) {
        this.resizeObserver = new ResizeObserver((entries) => {
          clearTimeout(this.resizeTimer);
          this.resizeTimer = setTimeout(() => {
            if (!this.isScrolling) {
              this.instances.forEach(carousel => {
                if (carousel.isVisible) {
                  carousel.measureAndUpdate();
                }
              });
            }
          }, 250);
        });
      }
    }

    setupGlobalEventListeners() {
      // Global scroll detection
      const handleScroll = () => {
        this.isScrolling = true;
        clearTimeout(this.scrollTimer);
        this.scrollTimer = setTimeout(() => {
          this.isScrolling = false;
        }, 150);
      };

      window.addEventListener('scroll', handleScroll, { passive: true });
      window.addEventListener('touchmove', handleScroll, { passive: true });

      // Fallback resize handler if ResizeObserver is not available
      if (!window.ResizeObserver) {
        window.addEventListener('resize', () => {
          clearTimeout(this.resizeTimer);
          this.resizeTimer = setTimeout(() => {
            if (!this.isScrolling) {
              this.instances.forEach(carousel => {
                if (carousel.isVisible) {
                  carousel.measureAndUpdate();
                }
              });
            }
          }, 250);
        }, { passive: true });
      }
    }

    // Single animation loop for all carousels
    startGlobalAnimation() {
      if (this.isAnimating) return;
      this.isAnimating = true;
      
      const animate = () => {
        if (!this.isAnimating) return;
        
        let hasActiveCarousels = false;
        this.instances.forEach(carousel => {
          if (carousel.isVisible && carousel.isRunning) {
            carousel.updatePosition();
            hasActiveCarousels = true;
          }
        });

        if (hasActiveCarousels) {
          this.animationId = requestAnimationFrame(animate);
        } else {
          this.isAnimating = false;
          this.animationId = null;
        }
      };
      
      this.animationId = requestAnimationFrame(animate);
    }

    stopGlobalAnimation() {
      this.isAnimating = false;
      if (this.animationId) {
        cancelAnimationFrame(this.animationId);
        this.animationId = null;
      }
    }

    updateAnimationState() {
      const hasVisibleCarousels = Array.from(this.instances.values()).some(c => c.isVisible);
      if (hasVisibleCarousels && !this.isAnimating) {
        this.startGlobalAnimation();
      } else if (!hasVisibleCarousels && this.isAnimating) {
        this.stopGlobalAnimation();
      }
    }

    addCarousel(container, carousel) {
      this.instances.set(container, carousel);
      this.intersectionObserver.observe(container);
      if (this.resizeObserver) {
        this.resizeObserver.observe(container);
      }
      this.updateAnimationState();
    }

    removeCarousel(container) {
      const carousel = this.instances.get(container);
      if (carousel) {
        this.intersectionObserver.unobserve(container);
        if (this.resizeObserver) {
          this.resizeObserver.unobserve(container);
        }
        this.instances.delete(container);
        this.updateAnimationState();
      }
    }
  }

  // Create global manager instance
  if (!window.__carouselManager) {
    window.__carouselManager = new CarouselManager();
  }
  const manager = window.__carouselManager;
  
  class InfiniteCarousel {
    constructor(container, options = {}) {
      this.container = container;
      this.track = container.querySelector('.element-carousel-track');
      this.options = {
        speed: options.speed || 1,
        gap: options.gap || 15,
        ...options
      };
      
      this.currentX = 0;
      this.elements = [];
      this.clones = [];
      this.containerWidth = 0;
      this.contentWidth = 0;
      this.isRunning = true;
      this.isVisible = true;
      this.lastMeasureTime = 0;
      
      this.init();
    }
    
    init() {
      this.elements = Array.from(this.track.children);
      if (this.elements.length === 0) return;
      
      this.track.style.gap = `${this.options.gap}px`;
      this.measureAndUpdate();
      
      // Register with global manager
      manager.addCarousel(this.container, this);
    }
    
    measureAndUpdate() {
      const now = Date.now();
      if (now - this.lastMeasureTime < 100) return;
      this.lastMeasureTime = now;
      
      this.measureDimensions();
      this.createClones();
    }
    
    measureDimensions() {
      this.containerWidth = this.container.offsetWidth;
      
      this.contentWidth = 0;
      this.elements.forEach(element => {
        if (element.offsetWidth > 0) {
          this.contentWidth += element.offsetWidth + this.options.gap;
        }
      });
      this.contentWidth = Math.max(this.contentWidth - this.options.gap, 100);
    }
    
    createClones() {
      // Clean up old clones
      this.clones.forEach(clone => clone.remove());
      this.clones = [];
      
      if (this.contentWidth === 0) return;
      
      const totalNeeded = Math.ceil((this.containerWidth * 2.5) / this.contentWidth) + 1;
      
      for (let i = 0; i < totalNeeded; i++) {
        this.elements.forEach(element => {
          const clone = element.cloneNode(true);
          clone.classList.add('carousel-clone');
          this.track.appendChild(clone);
          this.clones.push(clone);
        });
      }
    }
    
    // Called by global animation loop
    updatePosition() {
      if (!this.isRunning || !this.isVisible || this.contentWidth === 0) return;
      
      this.currentX -= this.options.speed * 0.5;
      
      if (Math.abs(this.currentX) >= this.contentWidth + this.options.gap) {
        this.currentX = 0;
      }
      
      this.track.style.transform = `translateX(${this.currentX}px)`;
    }
    
    destroy() {
      manager.removeCarousel(this.container);
      this.clones.forEach(clone => clone.remove());
      this.clones = [];
    }
  }
  
  function init() {
    const containers = document.querySelectorAll('[data-element-carousel]');
    
    if(!containers || containers.length === 0) {
      return;
    }
    
    containers.forEach((container) => {
      // Skip if already initialized
      if(container._carouselInstance && container.querySelector('.element-carousel-track')) {
        return;
      }
      
      const validChildren = Array.from(container.children).filter(child => 
        !child.classList.contains('element-carousel-preview') && 
        child.tagName !== 'SCRIPT'
      );
      
      if(validChildren.length === 0) {
        return;
      }
      
      if(container._carouselInstance) {
        container._carouselInstance.destroy();
      }
      
      // Create the carousel structure
      const preview = document.createElement('div');
      preview.className = 'element-carousel-preview';
      
      const track = document.createElement('div');
      track.className = 'element-carousel-track';
      
      // Move existing child elements to the track
      validChildren.forEach((child) => {
        track.appendChild(child.cloneNode(true));
      });
      
      const leftBlur = document.createElement('div');
      leftBlur.className = 'carousel-blur-left';
      const rightBlur = document.createElement('div');
      rightBlur.className = 'carousel-blur-right';
      
      preview.appendChild(leftBlur);
      preview.appendChild(track);
      preview.appendChild(rightBlur);
      
      // Clear container and add carousel structure
      container.innerHTML = '';
      container.appendChild(preview);
      
      // Mark as initialized and create instance immediately
      container.setAttribute('data-carousel-initialized', 'true');
      
      try {
        container._carouselInstance = new InfiniteCarousel(preview, {
          speed: 1,
          gap: 15
        });
      } catch(error) {
        // Silent error handling
      }
    });
  }
  
  // Immediate initialization
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
  
  document.addEventListener('bricks/content_loaded', init);
})();

Next-level design with Loomia.

A premium template crafted for agencies, creators, and teams who need stunning design without complexity or wasted time.

Start Creating
Book a 15-min intro call
Available now
function createPulseDot() {
    const elements = document.querySelectorAll('[data-pulse-dot]:not([data-pulse-initialized])');
    
    elements.forEach(element => {
        element.setAttribute('data-pulse-initialized', 'true');
        
        // Get custom attributes or use defaults
        const size = element.getAttribute('data-pulse-size') || '7';
        const color = element.getAttribute('data-pulse-color') || '#22c55e';
        const speed = element.getAttribute('data-pulse-speed') || '1.5';
        const scale = element.getAttribute('data-pulse-scale') || '2';
        const opacityRaw = element.getAttribute('data-pulse-opacity') || '100';
        const opacity = parseInt(opacityRaw) / 100;
        const spacing = element.getAttribute('data-pulse-spacing') || '8';
        const shadowEnabled = element.getAttribute('data-pulse-shadow-enabled') !== null ? 
            element.getAttribute('data-pulse-shadow-enabled') === 'true' : true;
        const shadowBlur = element.getAttribute('data-pulse-shadow-blur') || '10';
        const animationStyle = element.getAttribute('data-pulse-animation-style') || 'fade';
        const timingFunction = element.getAttribute('data-pulse-timing-function') || 'cubic-bezier(0.4, 0, 0.6, 1)';
        const pauseOnHover = element.getAttribute('data-pulse-pause-hover') !== null ? 
            element.getAttribute('data-pulse-pause-hover') === 'true' : false;
        
        // Create dot element
        const dot = document.createElement('span');
        dot.className = 'status-pulse-dot';
        dot.style.cssText = `
            position: relative;
            display: inline-block;
            width: ${size}px;
            height: ${size}px;
            background: ${color};
            border-radius: 50%;
            margin-right: ${spacing}px;
            vertical-align: middle;
            opacity: ${opacity};
            ${shadowEnabled ? `box-shadow: 0 0 ${shadowBlur}px ${color};` : ''}
        `;
        
        // Generate unique animation name
        const animationId = 'pulse_' + Math.random().toString(36).substr(2, 9);
        
        // Create animations based on style
        let keyframes = '';
        if (animationStyle === 'fade') {
            keyframes = `
                @keyframes ${animationId} {
                    0% { transform: scale(1); opacity: ${opacity}; }
                    100% { transform: scale(${scale}); opacity: 0; }
                }
            `;
            const pulse = document.createElement('span');
            pulse.style.cssText = `
                position: absolute;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                background: inherit;
                border-radius: inherit;
                animation: ${animationId} ${speed}s ${timingFunction} infinite;
            `;
            dot.appendChild(pulse);
        } else if (animationStyle === 'grow') {
            keyframes = `
                @keyframes ${animationId} {
                    0%, 100% { transform: scale(1); }
                    50% { transform: scale(${scale}); }
                }
            `;
            dot.style.animation = `${animationId} ${speed}s ${timingFunction} infinite`;
        } else if (animationStyle === 'both') {
            keyframes = `
                @keyframes ${animationId} {
                    0% { transform: scale(1); opacity: ${opacity}; }
                    50% { transform: scale(${scale}); opacity: ${opacity / 2}; }
                    100% { transform: scale(1); opacity: ${opacity}; }
                }
            `;
            const pulse = document.createElement('span');
            pulse.style.cssText = `
                position: absolute;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                background: inherit;
                border-radius: inherit;
                animation: ${animationId} ${speed}s ${timingFunction} infinite;
            `;
            dot.appendChild(pulse);
        } else if (animationStyle === 'double') {
            keyframes = `
                @keyframes ${animationId} {
                    0% { transform: scale(1); opacity: ${opacity}; }
                    100% { transform: scale(${scale}); opacity: 0; }
                }
            `;
            for (let i = 0; i < 2; i++) {
                const pulse = document.createElement('span');
                pulse.style.cssText = `
                    position: absolute;
                    left: 0;
                    top: 0;
                    width: 100%;
                    height: 100%;
                    background: inherit;
                    border-radius: inherit;
                    animation: ${animationId} ${speed}s ${timingFunction} infinite;
                    ${i === 1 ? `animation-delay: ${parseFloat(speed) / 2}s;` : ''}
                `;
                dot.appendChild(pulse);
            }
        }
        
        // Inject keyframes
        if (keyframes) {
            const style = document.createElement('style');
            style.textContent = keyframes;
            document.head.appendChild(style);
        }
        
        // Add pause on hover functionality
        if (pauseOnHover) {
            element.addEventListener('mouseenter', () => {
                dot.style.animationPlayState = 'paused';
                dot.querySelectorAll('span').forEach(span => {
                    span.style.animationPlayState = 'paused';
                });
            });
            
            element.addEventListener('mouseleave', () => {
                dot.style.animationPlayState = 'running';
                dot.querySelectorAll('span').forEach(span => {
                    span.style.animationPlayState = 'running';
                });
            });
        }
        
        // Insert dot and setup element display
        element.insertBefore(dot, element.firstChild);
        element.style.display = 'inline-flex';
        element.style.alignItems = 'center';
    });
}

// Initialize on DOM ready or immediately if already loaded
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', createPulseDot);
} else {
    createPulseDot();
}

// Also run on dynamic content changes (optional)
const observer = new MutationObserver(() => {
    createPulseDot();
});

observer.observe(document.body, {
    childList: true,
    subtree: true
});
(function(){
  const style = document.createElement('style');
  style.textContent = `
    .brand-carousel-preview {
      display: flex;
      align-items: center;
      overflow: hidden;
      position: relative;
      width: 100%;
      height: 100%;
    }
    
    .brand-carousel-track {
      display: flex;
      align-items: center;
      will-change: transform;
      padding: 0;
      position: relative;
      gap: 30px;
      backface-visibility: hidden;
      perspective: 1000px;
      transform: translateZ(0);
    }
    
    .brand-logo {
      height: 35px;
      width: auto;
      opacity: 0.9;
      transition: all 0.3s ease;
      display: block;
      max-width: none;
      filter: brightness(0.95) contrast(1.1);
      
      border-radius: 8px;
      flex-shrink: 0;
    }
    
    .brand-logo:hover {
      opacity: 1;
      transform: translateY(-2px);
      filter: brightness(1) contrast(1.2);
      
    }
    
    .carousel-blur-left,
    .carousel-blur-right {
      position: absolute;
      top: 0;
      bottom: 0;
      width: min(80px, 15%);
      pointer-events: none;
      z-index: 1;
    }
    
    .carousel-blur-left {
      left: 0;
      background: linear-gradient(to right, rgba(255,255,255,0.9), rgba(255,255,255,0));
    }
    
    .carousel-blur-right {
      right: 0;
      background: linear-gradient(to left, rgba(255,255,255,0.9), rgba(255,255,255,0));
    }
  `;
  document.head.appendChild(style);
  
  class InfiniteCarousel {
    constructor(container, options = {}) {
      this.container = container;
      this.track = container.querySelector('.brand-carousel-track');
      this.options = {
        speed: options.speed || 1,
        gap: options.gap || 30,
        ...options
      };
      
      this.animationId = null;
      this.currentX = 0;
      this.logos = [];
      this.clones = [];
      this.containerWidth = 0;
      this.contentWidth = 0;
      this.isRunning = false;
      this.isVisible = true;
      this.lastMeasureTime = 0;
      
      this.init();
    }
    
    init() {
      this.logos = Array.from(this.track.children);
      if (this.logos.length === 0) return;
      
      this.preloadImages().then(() => {
        this.measureDimensions();
        this.createClones();
        this.setupIntersectionObserver();
        this.setupResizeHandler();
        this.start();
      });
    }
    
    updateConfig(newOptions) {
      Object.assign(this.options, newOptions);
      
      // Update dimensions and clones after config changes
      setTimeout(() => {
        this.measureDimensions();
        this.createClones();
      }, 50);
    }
    
    preloadImages() {
      const images = this.logos.filter(logo => logo.tagName === 'IMG');
      const promises = images.map(img => {
        return new Promise((resolve) => {
          if (img.complete && img.naturalWidth > 0) {
            resolve();
          } else {
            const handleLoad = () => {
              img.removeEventListener('load', handleLoad);
              img.removeEventListener('error', handleError);
              resolve();
            };
            
            const handleError = () => {
              img.removeEventListener('load', handleLoad);
              img.removeEventListener('error', handleError);
              if (img.src.includes('.svg')) {
                const fallbackSrc = img.src.replace('.svg', '.png');
                if (fallbackSrc !== img.src) {
                  img.src = fallbackSrc;
                  img.addEventListener('load', handleLoad);
                  img.addEventListener('error', () => resolve());
                } else {
                  resolve();
                }
              } else {
                resolve();
              }
            };
            
            img.addEventListener('load', handleLoad);
            img.addEventListener('error', handleError);
            
            setTimeout(() => {
              if (!img.complete) {
                handleError();
              }
            }, 3000);
          }
        });
      });
      
      return Promise.all(promises);
    }
    
    setupIntersectionObserver() {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          this.isVisible = entry.isIntersecting;
          if (!this.isVisible) {
            this.pause();
          } else {
            this.resume();
          }
        });
      }, {
        threshold: 0.1,
        rootMargin: '50px'
      });
      
      observer.observe(this.container);
      this.intersectionObserver = observer;
    }
    
    setupResizeHandler() {
      let resizeTimer = null;
      let lastWidth = this.container.offsetWidth;
      
      const handleResize = () => {
        const currentWidth = this.container.offsetWidth;
        if (Math.abs(currentWidth - lastWidth) < 10) return;
        
        lastWidth = currentWidth;
        clearTimeout(resizeTimer);
        
        resizeTimer = setTimeout(() => {
          if (this.isVisible) {
            this.measureDimensions();
            this.createClones();
            // Ensure animation continues after resize
            if (!this.isRunning && this.isVisible) {
              this.start();
            }
          }
        }, 250);
      };
      
      window.addEventListener('resize', handleResize, { passive: true });
      this.resizeHandler = handleResize;
    }
    
    measureDimensions() {
      const now = Date.now();
      if (now - this.lastMeasureTime < 100) return;
      this.lastMeasureTime = now;
      
      this.containerWidth = this.container.offsetWidth;
      
      this.contentWidth = 0;
      this.logos.forEach(logo => {
        if (logo.offsetWidth > 0) {
          this.contentWidth += logo.offsetWidth + this.options.gap;
        }
      });
      this.contentWidth = Math.max(this.contentWidth - this.options.gap, 100);
    }
    
    createClones() {
      this.clones.forEach(clone => clone.remove());
      this.clones = [];
      
      if (this.contentWidth === 0) return;
      
      const totalNeeded = Math.ceil((this.containerWidth * 2.5) / this.contentWidth) + 1;
      
      for (let i = 0; i < totalNeeded; i++) {
        this.logos.forEach(logo => {
          const clone = logo.cloneNode(true);
          clone.classList.add('carousel-clone');
          this.track.appendChild(clone);
          this.clones.push(clone);
        });
      }
    }
    
    start() {
      if (this.isRunning || !this.isVisible) return;
      this.isRunning = true;
      this.animate();
    }
    
    pause() {
      this.isRunning = false;
      if (this.animationId) {
        cancelAnimationFrame(this.animationId);
        this.animationId = null;
      }
    }
    
    resume() {
      if (!this.isRunning && this.isVisible) {
        this.start();
      }
    }
    
    stop() {
      this.pause();
    }
    
    animate() {
      if (!this.isRunning || !this.isVisible) return;
      
      this.currentX -= this.options.speed * 0.5;
      
      if (Math.abs(this.currentX) >= this.contentWidth + this.options.gap) {
        this.currentX = 0;
      }
      
      this.track.style.transform = `translateX(${this.currentX}px)`;
      
      this.animationId = requestAnimationFrame(() => this.animate());
    }
    
    destroy() {
      this.stop();
      this.clones.forEach(clone => clone.remove());
      this.clones = [];
      
      if (this.intersectionObserver) {
        this.intersectionObserver.disconnect();
      }
      
      if (this.resizeHandler) {
        window.removeEventListener('resize', this.resizeHandler);
      }
    }
  }
  
  function init() {
    const c = document.querySelector('[data-brand-carousel]');
    if(!c) return;
    
    if(c._carouselInstance) {
      c._carouselInstance.destroy();
    }
    c.innerHTML = '';
    
    const preview = document.createElement('div');
    preview.className = 'brand-carousel-preview';
    
    const track = document.createElement('div');
    track.className = 'brand-carousel-track';
    
    const leftBlur = document.createElement('div');
    leftBlur.className = 'carousel-blur-left';
    const rightBlur = document.createElement('div');
    rightBlur.className = 'carousel-blur-right';
    
              c.setAttribute('data-brand-1', 'https://cdn.svglogos.dev/logos/apidog.svg');
              c.setAttribute('data-brand-2', 'https://cdn.svglogos.dev/logos/importio.svg');
              c.setAttribute('data-brand-3', 'https://cdn.svglogos.dev/logos/claude.svg');
              c.setAttribute('data-brand-4', 'https://cdn.svglogos.dev/logos/perplexity.svg');
              c.setAttribute('data-brand-5', 'https://cdn.svglogos.dev/logos/biomejs.svg');
    
    let logos = [];
    for(let i = 1; i <= 8; i++) {
      const src = c.getAttribute(`data-brand-${i}`);
      if(src) {
        logos.push({
          src: src,
          alt: `Brand ${i}`
        });
      }
    }
    
    if (logos.length === 0) {
      logos = [
        { src: "https://www.svgrepo.com/show/303205/html-5-logo.svg", alt: "HTML5" },
        { src: "https://www.svgrepo.com/show/303481/css-3-logo.svg", alt: "CSS3" },
        { src: "https://www.svgrepo.com/show/303206/javascript-logo.svg", alt: "JavaScript" },
        { src: "https://www.svgrepo.com/show/303266/nodejs-icon-logo.svg", alt: "Node.js" }
      ];
    }
    
    logos.forEach(logo => {
      const img = document.createElement('img');
      img.src = logo.src;
      img.alt = logo.alt;
      img.className = 'brand-logo';
      track.appendChild(img);
    });
    
    preview.appendChild(leftBlur);
    preview.appendChild(track);
    preview.appendChild(rightBlur);
    c.appendChild(preview);
    
    setTimeout(() => {
      c._carouselInstance = new InfiniteCarousel(preview, {
        speed: 1,
        gap: 30
      });
    }, 100);
  }
  
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
  
  document.addEventListener('bricks/content_loaded', init);
  
  setTimeout(init, 100);
})();
Getting Started

Get stunning design in three easy steps.

Popular
$2,995/month
Join today
Choose

Pick the perfect template and start right away. No setup, no stress, just design ready to go.

Customize

Easily adjust sections, styles, and content to match your brand. Everything is built to be flexible.

(function() {
  window.BricksFolderAnimation = window.BricksFolderAnimation || {};
  
  const defaultConfig = {
    folderSize: 1,
    folderItems: 3,
    hoverSpeed: 0.3,
    folderColor: "#1E40AF",
    paper1Color: "#E6E6E6",
    paper2Color: "#F2F2F2",
    paper3Color: "#FFFFFF",
    paper1Image: "https://colorlib.com/wp/wp-content/uploads/sites/2/academia-free-template.jpg",
    paper2Image: "https://colorlib.com/wp/wp-content/uploads/sites/2/videograph-free-template-408x322.jpg",
    paper3Image: "https://elements-resized.envatousercontent.com/elements-cover-images/42ac42a4-163a-403e-bd51-2ffc897f1092?w=433&cf_fit=scale-down&q=85&format=auto&s=ff79197efbf1b4b3d0eccbd5f62edd1e723c4578564ac4d532ac7ab1eda60184",
    imageFit: "cover",
    glowIntensity: 0,
    particleEffect: "none",
    entranceAnimation: "none",
    magneticEffect: "none"
  };
  
  const darkenColor = (hex, percent) => {
    let color = hex.startsWith("#") ? hex.slice(1) : hex;
    if (color.length === 3) {
      color = color
        .split("")
        .map((c) => c + c)
        .join("");
    }
    const num = parseInt(color, 16);
    let r = (num >> 16) & 0xff;
    let g = (num >> 8) & 0xff;
    let b = num & 0xff;
    r = Math.max(0, Math.min(255, Math.floor(r * (1 - percent))));
    g = Math.max(0, Math.min(255, Math.floor(g * (1 - percent))));
    b = Math.max(0, Math.min(255, Math.floor(b * (1 - percent))));
    return (
      "#" +
      ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()
    );
  };

  const createStyles = () => {
    const styleElement = document.createElement('style');
    styleElement.id = 'bricks-folder-animation-styles';
    
    styleElement.textContent = `
      div[data-folder].bricks-folder {
        position: relative !important;
        display: flex !important;
        justify-content: center !important;
        align-items: center !important;
        overflow: visible !important;
        box-sizing: border-box !important;
        padding: 1rem !important;
      }
      
      .bricks-folder.glow .folder {
        filter: drop-shadow(0 0 var(--glow-size, 10px) var(--folder-color));
      }
      
      .bricks-folder .folder {
        transition: all var(--folder-speed, ${defaultConfig.hoverSpeed}s) ease-in;
        cursor: pointer;
      }
      
      .bricks-folder .folder.entrance-fadeIn {
        animation: folderFadeIn 1s ease-out;
      }
      
      .bricks-folder .folder.entrance-slideUp {
        animation: folderSlideUp 0.8s ease-out;
      }
      
      .bricks-folder .folder.entrance-bounce {
        animation: folderBounce 1.2s ease-out;
      }
      
      .bricks-folder .folder.entrance-rotate {
        animation: folderRotate 1s ease-out;
      }
      
      @keyframes folderFadeIn {
        from { opacity: 0; transform: scale(0.8); }
        to { opacity: 1; transform: scale(var(--folder-scale, 1)); }
      }
      
      @keyframes folderSlideUp {
        from { opacity: 0; transform: translateY(50px) scale(var(--folder-scale, 1)); }
        to { opacity: 1; transform: translateY(0) scale(var(--folder-scale, 1)); }
      }
      
      @keyframes folderBounce {
        0% { opacity: 0; transform: scale(0.3) scale(var(--folder-scale, 1)); }
        50% { opacity: 1; transform: scale(1.1) scale(var(--folder-scale, 1)); }
        70% { transform: scale(0.9) scale(var(--folder-scale, 1)); }
        100% { transform: scale(1) scale(var(--folder-scale, 1)); }
      }
      
      @keyframes folderRotate {
        from { opacity: 0; transform: rotate(-180deg) scale(0.5) scale(var(--folder-scale, 1)); }
        to { opacity: 1; transform: rotate(0deg) scale(1) scale(var(--folder-scale, 1)); }
      }
      
      .bricks-folder .folder:not(.folder--click):hover {
        transform: translateY(-8px);
      }
      
      .bricks-folder .folder:not(.folder--click):hover .paper {
        transform: translate(-50%, 0%);
      }
      
      .bricks-folder .folder:not(.folder--click):hover .folder__front {
        transform: skew(15deg) scaleY(0.6);
      }
      
      .bricks-folder .folder:not(.folder--click):hover .right {
        transform: skew(-15deg) scaleY(0.6);
      }
      
      .bricks-folder .folder.open {
        transform: translateY(-8px);
      }
      
      .bricks-folder .folder.open .paper:nth-child(1) {
        transform: translate(-120%, -70%) rotateZ(-15deg);
      }
      
      .bricks-folder .folder.open .paper:nth-child(1):hover {
        transform: translate(-120%, -70%) rotateZ(-15deg) scale(1.1);
      }
      
      .bricks-folder .folder.open .paper:nth-child(2) {
        transform: translate(10%, -70%) rotateZ(15deg);
        height: 80%;
      }
      
      .bricks-folder .folder.open .paper:nth-child(2):hover {
        transform: translate(10%, -70%) rotateZ(15deg) scale(1.1);
      }
      
      .bricks-folder .folder.open .paper:nth-child(3) {
        transform: translate(-50%, -100%) rotateZ(5deg);
        height: 80%;
      }
      
      .bricks-folder .folder.open .paper:nth-child(3):hover {
        transform: translate(-50%, -100%) rotateZ(5deg) scale(1.1);
      }
      
      .bricks-folder .folder.open .folder__front {
        transform: skew(15deg) scaleY(0.6);
      }
      
      .bricks-folder .folder.open .right {
        transform: skew(-15deg) scaleY(0.6);
      }
      
      .bricks-folder .paper.magnetic {
        transform: translate(-50%, 10%) translate(var(--magnet-x, 0), var(--magnet-y, 0));
      }
      
      .bricks-folder .folder.open .paper.magnetic:nth-child(1) {
        transform: translate(-120%, -70%) rotateZ(-15deg) translate(var(--magnet-x, 0), var(--magnet-y, 0));
      }
      
      .bricks-folder .folder.open .paper.magnetic:nth-child(2) {
        transform: translate(10%, -70%) rotateZ(15deg) translate(var(--magnet-x, 0), var(--magnet-y, 0));
      }
      
      .bricks-folder .folder.open .paper.magnetic:nth-child(3) {
        transform: translate(-50%, -100%) rotateZ(5deg) translate(var(--magnet-x, 0), var(--magnet-y, 0));
      }
      
      .bricks-folder .folder__back {
        position: relative;
        width: 100px;
        height: 80px;
        background: var(--folder-back-color);
        border-radius: 0px 10px 10px 10px;
      }
      
      .bricks-folder .folder__back::after {
        position: absolute;
        z-index: 0;
        bottom: 98%;
        left: 0;
        content: "";
        width: 30px;
        height: 10px;
        background: var(--folder-back-color);
        border-radius: 5px 5px 0 0;
      }
      
      .bricks-folder .paper {
        position: absolute;
        z-index: 2;
        bottom: 10%;
        left: 50%;
        transform: translate(-50%, 10%);
        width: 70%;
        height: 80%;
        background: var(--paper-1);
        border-radius: 10px;
        transition: all var(--folder-speed, ${defaultConfig.hoverSpeed}s) ease-in-out;
        background-size: var(--paper-size, ${defaultConfig.imageFit});
        background-position: center;
        background-repeat: no-repeat;
      }
      
      .bricks-folder .paper:nth-child(2) {
        background: var(--paper-2);
        background-size: var(--paper-size, ${defaultConfig.imageFit});
        background-position: center;
        background-repeat: no-repeat;
        width: 80%;
        height: 70%;
      }
      
      .bricks-folder .paper:nth-child(3) {
        background: var(--paper-3);
        background-size: var(--paper-size, ${defaultConfig.imageFit});
        background-position: center;
        background-repeat: no-repeat;
        width: 90%;
        height: 60%;
      }
      
      .bricks-folder .folder__front {
        position: absolute;
        z-index: 3;
        width: 100%;
        height: 100%;
        background: var(--folder-color);
        border-radius: 5px 10px 10px 10px;
        transform-origin: bottom;
        transition: all var(--folder-speed, ${defaultConfig.hoverSpeed}s) ease-in-out;
      }
      
      .bricks-folder .folder__front.right {
        width: 50%;
        right: 0;
      }
      
      .particle {
        position: absolute;
        pointer-events: none;
        z-index: 1000;
      }
      
      .particle.sparkle {
        width: 4px;
        height: 4px;
        background: radial-gradient(circle, #fff 0%, transparent 70%);
        border-radius: 50%;
        animation: sparkleFloat 2s ease-out forwards;
      }
      
      .particle.dot {
        width: 3px;
        height: 3px;
        background: var(--folder-color);
        border-radius: 50%;
        animation: dotFloat 1.5s ease-out forwards;
      }
      
      .particle.star {
        width: 6px;
        height: 6px;
        background: #ffd700;
        clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
        animation: starFloat 2.5s ease-out forwards;
      }
      
      @keyframes sparkleFloat {
        0% { opacity: 1; transform: translate(0, 0) scale(0); }
        50% { opacity: 1; transform: translate(var(--random-x, 0), var(--random-y, 0)) scale(1); }
        100% { opacity: 0; transform: translate(var(--random-x, 0), var(--random-y, 0)) scale(0); }
      }
      
      @keyframes dotFloat {
        0% { opacity: 1; transform: translate(0, 0) scale(1); }
        100% { opacity: 0; transform: translate(var(--random-x, 0), var(--random-y, 0)) scale(0); }
      }
      
      @keyframes starFloat {
        0% { opacity: 1; transform: translate(0, 0) scale(0) rotate(0deg); }
        50% { opacity: 1; transform: translate(var(--random-x, 0), var(--random-y, 0)) scale(1) rotate(180deg); }
        100% { opacity: 0; transform: translate(var(--random-x, 0), var(--random-y, 0)) scale(0) rotate(360deg); }
      }
      
      @media (prefers-reduced-motion: reduce) {
        .bricks-folder .folder,
        .bricks-folder .paper,
        .bricks-folder .folder__front {
          transition: none !important;
        }
      }
    `;
    
    document.head.appendChild(styleElement);
  };

  const initFolderAnimation = () => {
    if (!document.getElementById('bricks-folder-animation-styles')) {
      createStyles();
    }

    const folderElements = document.querySelectorAll('div[data-folder]');
    
    folderElements.forEach(element => {
      if (element.hasAttribute('data-folder-initialized')) {
        return;
      }
      
      element.setAttribute('data-folder-initialized', 'true');
      
      const color = element.getAttribute('data-folder-color') || defaultConfig.folderColor;
      const size = parseFloat(element.getAttribute('data-folder-size') || defaultConfig.folderSize);
      const itemsCount = Math.min(3, Math.max(1, parseInt(element.getAttribute('data-folder-items') || defaultConfig.folderItems, 10)));
      const speed = parseFloat(element.getAttribute('data-folder-speed') || defaultConfig.hoverSpeed);
      const glowIntensity = parseInt(element.getAttribute('data-folder-glow') || defaultConfig.glowIntensity);
      const particleEffect = element.getAttribute('data-folder-particles') || defaultConfig.particleEffect;
      const entranceAnimation = element.getAttribute('data-folder-entrance') || defaultConfig.entranceAnimation;
      const magneticEffect = element.getAttribute('data-folder-magnetic') || defaultConfig.magneticEffect;
      
      const paper1Color = element.getAttribute('data-folder-paper1-color') || defaultConfig.paper1Color;
      const paper2Color = element.getAttribute('data-folder-paper2-color') || defaultConfig.paper2Color;
      const paper3Color = element.getAttribute('data-folder-paper3-color') || defaultConfig.paper3Color;
      
      const folderBackColor = darkenColor(color, 0.08);
      
      element.classList.add('bricks-folder');
      if (glowIntensity > 0) {
        element.classList.add('glow');
        element.style.setProperty('--glow-size', `${glowIntensity / 5}px`);
      } else {
        element.classList.remove('glow');
      }
      
      element.innerHTML = '';
      
      const folderContainer = document.createElement('div');
      folderContainer.className = 'folder';
      folderContainer.style.setProperty('--folder-color', color);
      folderContainer.style.setProperty('--folder-back-color', folderBackColor);
      folderContainer.style.setProperty('--paper-1', paper1Color);
      folderContainer.style.setProperty('--paper-2', paper2Color);
      folderContainer.style.setProperty('--paper-3', paper3Color);
      folderContainer.style.setProperty('--folder-speed', speed + 's');
      folderContainer.style.setProperty('--folder-scale', size);
      folderContainer.style.setProperty('--paper-size', defaultConfig.imageFit);
      folderContainer.style.transform = `scale(${size})`;
      
      if (entranceAnimation !== 'none') {
        folderContainer.classList.add(`entrance-${entranceAnimation}`);
      }
      
      const folderBack = document.createElement('div');
      folderBack.className = 'folder__back';
      
      const papers = [];
      for (let i = 0; i < itemsCount; i++) {
        const paper = document.createElement('div');
        paper.className = `paper paper-${i + 1}`;
        if (magneticEffect !== 'none') {
          paper.classList.add('magnetic');
        }
        paper.dataset.index = i;
        
        // Apply images from config
        const imageUrl = i === 0 ? defaultConfig.paper1Image : i === 1 ? defaultConfig.paper2Image : defaultConfig.paper3Image;
        if (imageUrl) {
          paper.style.backgroundImage = `url("${imageUrl}")`;
        }
        
        papers.push(paper);
        folderBack.appendChild(paper);
      }
      
      const folderFront = document.createElement('div');
      folderFront.className = 'folder__front';
      
      const folderFrontRight = document.createElement('div');
      folderFrontRight.className = 'folder__front right';
      
      folderBack.appendChild(folderFront);
      folderBack.appendChild(folderFrontRight);
      folderContainer.appendChild(folderBack);
      element.appendChild(folderContainer);
      
      let isOpen = false;
      
      if (magneticEffect !== 'none') {
        const magneticStrength = {
          'subtle': 0.05,
          'medium': 0.15,
          'strong': 0.25
        }[magneticEffect];
        
        papers.forEach((paper, index) => {
          paper.addEventListener('mousemove', (e) => {
            if (!isOpen) return;
            
            const rect = paper.getBoundingClientRect();
            const centerX = rect.left + rect.width / 2;
            const centerY = rect.top + rect.height / 2;
            const offsetX = (e.clientX - centerX) * magneticStrength;
            const offsetY = (e.clientY - centerY) * magneticStrength;
            
            paper.style.setProperty('--magnet-x', `${offsetX}px`);
            paper.style.setProperty('--magnet-y', `${offsetY}px`);
          });
          
          paper.addEventListener('mouseleave', () => {
            paper.style.setProperty('--magnet-x', '0px');
            paper.style.setProperty('--magnet-y', '0px');
          });
        });
      }
      
      function createParticles(x, y) {
        if (particleEffect === 'none') return;
        
        const particleCount = 8;
        const containerRect = element.getBoundingClientRect();
        
        for (let i = 0; i < particleCount; i++) {
          const particle = document.createElement('div');
          particle.className = `particle ${particleEffect === 'sparkles' ? 'sparkle' : particleEffect === 'dots' ? 'dot' : 'star'}`;
          
          const randomX = (Math.random() - 0.5) * 100;
          const randomY = (Math.random() - 0.5) * 100;
          
          particle.style.position = 'absolute';
          particle.style.left = (x - containerRect.left) + 'px';
          particle.style.top = (y - containerRect.top) + 'px';
          particle.style.setProperty('--random-x', randomX + 'px');
          particle.style.setProperty('--random-y', randomY + 'px');
          particle.style.setProperty('--folder-color', color);
          particle.style.pointerEvents = 'none';
          particle.style.zIndex = '1000';
          
          element.appendChild(particle);
          
          setTimeout(() => {
            if (particle.parentNode) {
              particle.parentNode.removeChild(particle);
            }
          }, 2500);
        }
      }
      
      folderContainer.addEventListener('click', (e) => {
        isOpen = !isOpen;
        
        if (isOpen) {
          folderContainer.classList.add('open');
          
          const rect = folderContainer.getBoundingClientRect();
          const x = rect.left + rect.width / 2;
          const y = rect.top + rect.height / 2;
          createParticles(x, y);
        } else {
          folderContainer.classList.remove('open');
          
          if (magneticEffect !== 'none') {
            papers.forEach(paper => {
              paper.style.setProperty('--magnet-x', '0px');
              paper.style.setProperty('--magnet-y', '0px');
            });
          }
        }
      });
    });
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initFolderAnimation);
  } else {
    initFolderAnimation();
  }

  document.addEventListener('bricks/layout/loaded', initFolderAnimation);
  
  window.BricksFolderAnimation.init = initFolderAnimation;
})();
Launch

Export your finished design and publish it in minutes. Fast, seamless, and ready to impress.

Design trusted by creators worldwide.

Book a 15-min intro call
Available now
function createPulseDot() {
    const elements = document.querySelectorAll('[data-pulse-dot]:not([data-pulse-initialized])');
    
    elements.forEach(element => {
        element.setAttribute('data-pulse-initialized', 'true');
        
        // Get custom attributes or use defaults
        const size = element.getAttribute('data-pulse-size') || '7';
        const color = element.getAttribute('data-pulse-color') || '#22c55e';
        const speed = element.getAttribute('data-pulse-speed') || '1.5';
        const scale = element.getAttribute('data-pulse-scale') || '2';
        const opacityRaw = element.getAttribute('data-pulse-opacity') || '100';
        const opacity = parseInt(opacityRaw) / 100;
        const spacing = element.getAttribute('data-pulse-spacing') || '8';
        const shadowEnabled = element.getAttribute('data-pulse-shadow-enabled') !== null ? 
            element.getAttribute('data-pulse-shadow-enabled') === 'true' : true;
        const shadowBlur = element.getAttribute('data-pulse-shadow-blur') || '10';
        const animationStyle = element.getAttribute('data-pulse-animation-style') || 'fade';
        const timingFunction = element.getAttribute('data-pulse-timing-function') || 'cubic-bezier(0.4, 0, 0.6, 1)';
        const pauseOnHover = element.getAttribute('data-pulse-pause-hover') !== null ? 
            element.getAttribute('data-pulse-pause-hover') === 'true' : false;
        
        // Create dot element
        const dot = document.createElement('span');
        dot.className = 'status-pulse-dot';
        dot.style.cssText = `
            position: relative;
            display: inline-block;
            width: ${size}px;
            height: ${size}px;
            background: ${color};
            border-radius: 50%;
            margin-right: ${spacing}px;
            vertical-align: middle;
            opacity: ${opacity};
            ${shadowEnabled ? `box-shadow: 0 0 ${shadowBlur}px ${color};` : ''}
        `;
        
        // Generate unique animation name
        const animationId = 'pulse_' + Math.random().toString(36).substr(2, 9);
        
        // Create animations based on style
        let keyframes = '';
        if (animationStyle === 'fade') {
            keyframes = `
                @keyframes ${animationId} {
                    0% { transform: scale(1); opacity: ${opacity}; }
                    100% { transform: scale(${scale}); opacity: 0; }
                }
            `;
            const pulse = document.createElement('span');
            pulse.style.cssText = `
                position: absolute;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                background: inherit;
                border-radius: inherit;
                animation: ${animationId} ${speed}s ${timingFunction} infinite;
            `;
            dot.appendChild(pulse);
        } else if (animationStyle === 'grow') {
            keyframes = `
                @keyframes ${animationId} {
                    0%, 100% { transform: scale(1); }
                    50% { transform: scale(${scale}); }
                }
            `;
            dot.style.animation = `${animationId} ${speed}s ${timingFunction} infinite`;
        } else if (animationStyle === 'both') {
            keyframes = `
                @keyframes ${animationId} {
                    0% { transform: scale(1); opacity: ${opacity}; }
                    50% { transform: scale(${scale}); opacity: ${opacity / 2}; }
                    100% { transform: scale(1); opacity: ${opacity}; }
                }
            `;
            const pulse = document.createElement('span');
            pulse.style.cssText = `
                position: absolute;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                background: inherit;
                border-radius: inherit;
                animation: ${animationId} ${speed}s ${timingFunction} infinite;
            `;
            dot.appendChild(pulse);
        } else if (animationStyle === 'double') {
            keyframes = `
                @keyframes ${animationId} {
                    0% { transform: scale(1); opacity: ${opacity}; }
                    100% { transform: scale(${scale}); opacity: 0; }
                }
            `;
            for (let i = 0; i < 2; i++) {
                const pulse = document.createElement('span');
                pulse.style.cssText = `
                    position: absolute;
                    left: 0;
                    top: 0;
                    width: 100%;
                    height: 100%;
                    background: inherit;
                    border-radius: inherit;
                    animation: ${animationId} ${speed}s ${timingFunction} infinite;
                    ${i === 1 ? `animation-delay: ${parseFloat(speed) / 2}s;` : ''}
                `;
                dot.appendChild(pulse);
            }
        }
        
        // Inject keyframes
        if (keyframes) {
            const style = document.createElement('style');
            style.textContent = keyframes;
            document.head.appendChild(style);
        }
        
        // Add pause on hover functionality
        if (pauseOnHover) {
            element.addEventListener('mouseenter', () => {
                dot.style.animationPlayState = 'paused';
                dot.querySelectorAll('span').forEach(span => {
                    span.style.animationPlayState = 'paused';
                });
            });
            
            element.addEventListener('mouseleave', () => {
                dot.style.animationPlayState = 'running';
                dot.querySelectorAll('span').forEach(span => {
                    span.style.animationPlayState = 'running';
                });
            });
        }
        
        // Insert dot and setup element display
        element.insertBefore(dot, element.firstChild);
        element.style.display = 'inline-flex';
        element.style.alignItems = 'center';
    });
}

// Initialize on DOM ready or immediately if already loaded
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', createPulseDot);
} else {
    createPulseDot();
}

// Also run on dynamic content changes (optional)
const observer = new MutationObserver(() => {
    createPulseDot();
});

observer.observe(document.body, {
    childList: true,
    subtree: true
});
(function(){
  const style = document.createElement('style');
  style.textContent = `
    .avatar-orbit-overlay {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      z-index: 10;
      overflow: hidden;
    }
    
    .avatar-orbit-preview {
      width: 100%;
      height: 100%;
      position: relative;
      overflow: hidden;
    }
    
    .orbit-avatar {
      position: absolute;
      border-radius: 50%;
      border: 3px solid #ffffff;
      background-size: cover;
      background-position: center;
      transition: all 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94);
      cursor: pointer;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
      pointer-events: auto;
      z-index: 5;
    }
    
    [data-avatar-orbit] {
      position: relative;
    }
  `;
  document.head.appendChild(style);
  
  // Math utilities for smooth interpolation
  const lerp = (start, end, factor) => start + (end - start) * factor;
  
  // Responsive utility functions
  function getScaleFactor() {
    const width = window.innerWidth;
    if (width <= 480) return 0.5;      // Mobile small
    if (width <= 768) return 0.65;     // Mobile/tablet
    if (width <= 1024) return 0.8;     // Tablet landscape
    return 1.0;                        // Desktop
  }
  
  function getAvatarScaleFactor() {
    const width = window.innerWidth;
    if (width <= 768) return 0.7;      // Mobile: 70% of original size
    return 1.0;                        // Desktop: full size
  }
  
  function getVerticalPosition(desktopValue, mobileValue) {
    const width = window.innerWidth;
    if (width <= 768) return mobileValue;
    return desktopValue;
  }
  
  class AvatarOrbit {
    constructor(container, options = {}) {
      this.container = container;
      
      this.options = {
        avatarSize: options.avatarSize || 80,
        orbitRadius: options.orbitRadius || 380,
        verticalPosition: options.verticalPosition || 90,
        mobileVerticalPosition: options.mobileVerticalPosition || 70,
        totalAvatars: options.totalAvatars || 16,
        animationSpeed: options.animationSpeed || 1,
        smoothness: options.smoothness || 0.85,
        borderColor: options.borderColor || '#ffffff',
        borderWidth: options.borderWidth || 3,
        ...options
      };
      
      this.avatars = [];
      this.scrollY = 0;
      this.targetScrollY = 0;
      this.scrollVelocity = 0;
      this.lastScrollY = 0;
      this.isVisible = true;
      this.animationId = null;
      this.lastTime = performance.now();
      
      this.init();
    }
    
    init() {
      this.container.style.position = 'relative';
      
      const existingOverlay = this.container.querySelector('.avatar-orbit-overlay');
      if (existingOverlay) {
        existingOverlay.remove();
      }
      
      this.overlay = document.createElement('div');
      this.overlay.className = 'avatar-orbit-overlay';
      
      this.preview = document.createElement('div');
      this.preview.className = 'avatar-orbit-preview';
      this.overlay.appendChild(this.preview);
      
      this.container.appendChild(this.overlay);
      
      this.createAvatars();
      this.setupScrollListener();
      this.setupIntersectionObserver();
      this.setupResizeListener();
      this.startAnimationLoop();
      this.updatePositions();
    }
    
    createAvatars() {
      let avatarUrls = [];
      for(let i = 1; i <= 10; i++) {
        const src = this.container.getAttribute(`data-avatar-${i}`);
        if(src) {
          avatarUrls.push({ src: src, alt: `Avatar ${i}` });
        }
      }
      
      const hasCustomImages = avatarUrls.length > 0;
      let baseAvatars, totalAvatarsToShow;
      
      if (hasCustomImages) {
        baseAvatars = avatarUrls;
        totalAvatarsToShow = this.options.totalAvatars;
      } else {
        baseAvatars = [
          { src: "https://i.pravatar.cc/150?img=1", alt: "Avatar 1" },
          { src: "https://i.pravatar.cc/150?img=2", alt: "Avatar 2" },
          { src: "https://i.pravatar.cc/150?img=3", alt: "Avatar 3" },
          { src: "https://i.pravatar.cc/150?img=4", alt: "Avatar 4" },
          { src: "https://i.pravatar.cc/150?img=5", alt: "Avatar 5" },
          { src: "https://i.pravatar.cc/150?img=6", alt: "Avatar 6" }
        ];
        totalAvatarsToShow = this.options.totalAvatars;
      }
      
      const repeatedAvatars = [];
      for (let i = 0; i < totalAvatarsToShow; i++) {
        const sourceIndex = i % baseAvatars.length;
        repeatedAvatars.push(baseAvatars[sourceIndex]);
      }
      
      // Calculate responsive sizes
      const avatarScaleFactor = getAvatarScaleFactor();
      const scaledSize = this.options.avatarSize * avatarScaleFactor;
      
      repeatedAvatars.forEach((avatar, index) => {
        const avatarEl = document.createElement('div');
        avatarEl.className = 'orbit-avatar';
        avatarEl.style.width = `${scaledSize}px`;
        avatarEl.style.height = `${scaledSize}px`;
        avatarEl.style.backgroundImage = `url(${avatar.src})`;
        avatarEl.setAttribute('data-index', index);
        
        // Store current positions for smooth interpolation
        avatarEl._currentX = 0;
        avatarEl._currentY = 0;
        avatarEl._currentOpacity = 1;
        
        avatarEl.addEventListener('mouseenter', () => {
          avatarEl.style.transform = `translate3d(${avatarEl._currentX}px, ${avatarEl._currentY}px, 0) scale(1.1)`;
          avatarEl.style.boxShadow = '0 8px 25px rgba(0, 0, 0, 0.4)';
          avatarEl.style.zIndex = '100';
        });
        
        avatarEl.addEventListener('mouseleave', () => {
          avatarEl.style.transform = `translate3d(${avatarEl._currentX}px, ${avatarEl._currentY}px, 0) scale(1)`;
          avatarEl.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
          avatarEl.style.zIndex = '5';
        });
        
        this.preview.appendChild(avatarEl);
        this.avatars.push(avatarEl);
      });
    }
    
    setupScrollListener() {
      const updateScroll = () => {
        const currentY = window.pageYOffset;
        this.scrollVelocity = (currentY - this.lastScrollY) * 0.5;
        this.lastScrollY = currentY;
        this.targetScrollY = currentY;
      };
      
      window.addEventListener('scroll', updateScroll, { passive: true });
      this.scrollHandler = updateScroll;
    }
    
    setupIntersectionObserver() {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          this.isVisible = entry.isIntersecting;
        });
      }, {
        threshold: 0.1,
        rootMargin: '50px'
      });
      
      observer.observe(this.container);
      this.intersectionObserver = observer;
    }
    
    setupResizeListener() {
      let resizeTimer = null;
      
      const handleResize = () => {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(() => {
          this.updateAvatarSize();
          this.updatePositions();
        }, 100);
      };
      
      window.addEventListener('resize', handleResize, { passive: true });
      this.resizeHandler = handleResize;
    }
    
    updateAvatarSize() {
      const avatarScaleFactor = getAvatarScaleFactor();
      const scaledSize = this.options.avatarSize * avatarScaleFactor;
      
      this.avatars.forEach(avatar => {
        avatar.style.width = `${scaledSize}px`;
        avatar.style.height = `${scaledSize}px`;
      });
    }
    
    startAnimationLoop() {
      const animate = (currentTime) => {
        if (!this.isVisible) {
          this.animationId = requestAnimationFrame(animate);
          return;
        }
        
        const deltaTime = currentTime - this.lastTime;
        this.lastTime = currentTime;
        
        // Smooth scroll interpolation
        const smoothness = this.options.smoothness;
        this.scrollY = lerp(this.scrollY, this.targetScrollY + this.scrollVelocity, 1 - smoothness);
        this.scrollVelocity *= 0.95;
        
        this.updatePositions();
        this.animationId = requestAnimationFrame(animate);
      };
      
      this.animationId = requestAnimationFrame(animate);
    }
    
    updatePositions() {
      if (!this.isVisible || this.avatars.length === 0) return;
      
      const containerRect = this.container.getBoundingClientRect();
      
      // Get responsive values
      const scaleFactor = getScaleFactor();
      const avatarScaleFactor = getAvatarScaleFactor();
      const verticalPos = getVerticalPosition(this.options.verticalPosition, this.options.mobileVerticalPosition);
      
      const centerX = containerRect.width / 2;
      const centerY = containerRect.height * (verticalPos / 100);
      
      const orbitRadius = this.options.orbitRadius * scaleFactor;
      const avatarSize = this.options.avatarSize * avatarScaleFactor;
      
      const scrollProgress = this.scrollY * this.options.animationSpeed * 0.0008;
      
      const totalAvatars = this.avatars.length;
      let spacingRadians, startAngle;
      
      if (totalAvatars > 1) {
        spacingRadians = (2 * Math.PI) / totalAvatars;
        startAngle = Math.PI / 2;
      } else {
        spacingRadians = 0;
        startAngle = Math.PI / 2;
      }
      
      this.avatars.forEach((avatar, index) => {
        const angle = startAngle + (spacingRadians * index) + scrollProgress;
        
        const targetX = centerX + Math.cos(angle) * orbitRadius - (avatarSize / 2);
        const targetY = centerY - Math.sin(angle) * orbitRadius - (avatarSize / 2);
        
        // Smooth interpolation
        avatar._currentX = lerp(avatar._currentX, targetX, 0.15);
        avatar._currentY = lerp(avatar._currentY, targetY, 0.15);
        
        const isInBounds = avatar._currentY > -avatarSize && 
                         avatar._currentY < containerRect.height && 
                         avatar._currentX > -avatarSize && 
                         avatar._currentX < containerRect.width;
        
        const targetOpacity = isInBounds ? 1 : 0;
        avatar._currentOpacity = lerp(avatar._currentOpacity, targetOpacity, 0.1);
        
        avatar.style.transform = `translate3d(${avatar._currentX}px, ${avatar._currentY}px, 0)`;
        avatar.style.opacity = avatar._currentOpacity;
        avatar.style.pointerEvents = avatar._currentOpacity > 0.1 ? 'auto' : 'none';
      });
    }
    
    destroy() {
      if (this.animationId) {
        cancelAnimationFrame(this.animationId);
      }
      
      if (this.scrollHandler) {
        window.removeEventListener('scroll', this.scrollHandler);
      }
      
      if (this.resizeHandler) {
        window.removeEventListener('resize', this.resizeHandler);
      }
      
      if (this.intersectionObserver) {
        this.intersectionObserver.disconnect();
      }
      
      if (this.overlay && this.overlay.parentNode) {
        this.overlay.parentNode.removeChild(this.overlay);
      }
      
      this.avatars = [];
    }
  }
  
  function init() {
    const c = document.querySelector('[data-avatar-orbit]');
    if(!c) return;
    
    if(c._orbitInstance) {
      c._orbitInstance.destroy();
    }
    

    
    setTimeout(() => {
      c._orbitInstance = new AvatarOrbit(c, {
        avatarSize: 80,
        orbitRadius: 380,
        verticalPosition: 90,
        mobileVerticalPosition: 70,
        totalAvatars: 16,
        animationSpeed: 1,
        smoothness: 0.85,
        borderColor: '#ffffff',
        borderWidth: 3
      });
    }, 50);
  }
  
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
  
  document.addEventListener('bricks/content_loaded', init);
  
  setTimeout(init, 100);
})();
Features

Everything you need, nothing you don’t.

Effortless setup

Start designing instantly. No tech hurdles, no wasted time.

Total flexibility

Adapt every section to your brand with complete freedom.

Lightning-fast workflow

Build and launch stunning pages in record time.

Premium quality

Beautifully designed layouts crafted for professionals.

Full control

You decide what to keep, tweak, or change, it’s yours.

Scalable design

Grow and expand without redesigning from scratch.

Pitch Decks
Branding
Web Design
UX Design
Social Graphics
Testimonials

Real teams. Real results.

Alexia FranMarketing Lead, RelayOne

No meetings, no delays, no drama. Just smart design delivered when we needed it. I can't recommend Loomia enough.

Emma CollinsMarketing Director, Stellar Brands

I've loved working with Whenevr. I didn’t need to explain things twice. The design just showed up looking exactly how I pictured it.

Ryan MitchellFounder, PixelForge Studio

Every request was handled quickly and nailed on the first pass. Genuinely the most efficient design experience I've had.

Alexia FranMarketing Lead, RelayOne

No meetings, no delays, no drama. Just smart design delivered when we needed it. I can't recommend Loomia enough.

Emma CollinsMarketing Director, Stellar Brands

I've loved working with Whenevr. I didn’t need to explain things twice. The design just showed up looking exactly how I pictured it.

Ryan MitchellFounder, PixelForge Studio

Every request was handled quickly and nailed on the first pass. Genuinely the most efficient design experience I've had.

(function() {
  const sliderConfig = {
    dotColor: "#1E40AF",
    inactiveDotColor: "rgba(0, 0, 0, 0.3)",
    arrowColor: "#1E40AF",
    arrowBgColor: "#ffffff",
    arrowBgOpacity: 0.2,
    arrowVertical: "center",
    arrowDistance: 0.5,
    arrowSize: 48,
    showArrowBg: true,
    transitionSpeed: 0.6,
    autoplayInterval: 4,
    slideGap: 10,
    activeScale: 0.95,
    inactiveOpacity: 0.6,
    enableAutoplay: true,
    enableDrag: true,
    dragSensitivity: 80,
    showArrows: true,
    slidesPerView: 3,
    advanceStep: "1"
  };

  function initSlider() {
    const sliderContainers = document.querySelectorAll('[data-slider]:not([data-slider-initialized="true"])');
    
    sliderContainers.forEach(sliderContainer => {
      const originalSlides = Array.from(sliderContainer.querySelectorAll('[data-slide]'));
      const slidesCount = originalSlides.length;
      
      if (slidesCount === 0) return;
      
      let currentSlide = 0;
      let autoplayInterval;
      let isDragging = false;
      let startX = 0;
      let startY = 0;
      let currentX = 0;
      let currentY = 0;
      let initialTransform = 0;
      let hasDetectedDirection = false;
      let allowHorizontalDrag = false;

      // Detectar si es móvil
      const isMobile = window.innerWidth <= 768;
      const effectiveSlidesPerView = isMobile ? 1 : sliderConfig.slidesPerView;
      const effectiveAdvanceStep = sliderConfig.advanceStep === 'group' ? effectiveSlidesPerView : 1;

      sliderContainer.style.position = 'relative';
      sliderContainer.style.overflow = 'hidden';
      sliderContainer.style.paddingBottom = '4rem';
      sliderContainer.style.cursor = sliderConfig.enableDrag ? 'grab' : 'default';
      sliderContainer.style.userSelect = 'none';
      sliderContainer.style.touchAction = 'pan-y pinch-zoom';

      const slidesWrapper = document.createElement('div');
      slidesWrapper.style.cssText = `
          display: flex;
          transition: transform ${sliderConfig.transitionSpeed}s cubic-bezier(0.25, 0.8, 0.25, 1);
          width: 100%;
          height: 100%;
          will-change: transform;
          gap: ${sliderConfig.slideGap}px;
      `;

      // BUCLE INFINITO: Clonación inteligente para evitar espacios vacíos
      let allSlides = [...originalSlides];
      
      if (effectiveSlidesPerView > 1 && slidesCount >= effectiveSlidesPerView) {
        // Clonar slides al final para el bucle infinito
        const clonesNeeded = effectiveSlidesPerView - 1;
        for (let i = 0; i < clonesNeeded; i++) {
          const clone = originalSlides[i].cloneNode(true);
          clone.setAttribute('data-slide-clone', 'end');
          allSlides.push(clone);
        }
        
        // Clonar slides al inicio para navegación hacia atrás
        const startClones = [];
        for (let i = slidesCount - clonesNeeded; i < slidesCount; i++) {
          const clone = originalSlides[i].cloneNode(true);
          clone.setAttribute('data-slide-clone', 'start');
          startClones.push(clone);
        }
        allSlides = [...startClones, ...allSlides];
        
        // Ajustar índice inicial para compensar los clones del inicio
        currentSlide = clonesNeeded;
      }

      // Configurar ancho de slides
      const totalSlidesInView = allSlides.length;
      const slideWidthPercentage = effectiveSlidesPerView > 1 ? 
        (100 / effectiveSlidesPerView) : 100;

      allSlides.forEach((slide, index) => {
          slide.style.cssText = `
              width: calc(${slideWidthPercentage}% - ${sliderConfig.slideGap * (effectiveSlidesPerView - 1) / effectiveSlidesPerView}px);
              flex-shrink: 0;
              opacity: 1;
              transform: scale(1);
              transition: all ${sliderConfig.transitionSpeed}s cubic-bezier(0.25, 0.8, 0.25, 1);
              pointer-events: none;
          `;
          slidesWrapper.appendChild(slide);
      });

      sliderContainer.innerHTML = '';
      sliderContainer.appendChild(slidesWrapper);

      // Crear dots basados en slides originales únicamente
      const dotsContainer = document.createElement('div');
      dotsContainer.style.cssText = `
          position: absolute;
          bottom: 1.5rem;
          left: 50%;
          transform: translateX(-50%);
          display: flex;
          gap: 0.75rem;
          z-index: 10;
      `;

      for (let i = 0; i < slidesCount; i++) {
          const dot = document.createElement('button');
          dot.style.cssText = `
              width: ${i === 0 ? '1.5rem' : '0.375rem'};
              height: 0.375rem;
              border-radius: 9999px;
              background-color: ${i === 0 ? sliderConfig.dotColor : sliderConfig.inactiveDotColor};
              border: none;
              padding: 0;
              cursor: pointer;
              transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
              -webkit-tap-highlight-color: transparent;
              touch-action: manipulation;
          `;
          
          dot.addEventListener('click', () => {
            const targetIndex = effectiveSlidesPerView > 1 && slidesCount >= effectiveSlidesPerView ? 
              i + (effectiveSlidesPerView - 1) : i;
            goToSlide(targetIndex);
          });
          dot.addEventListener('touchend', (e) => {
            e.preventDefault();
            const targetIndex = effectiveSlidesPerView > 1 && slidesCount >= effectiveSlidesPerView ? 
              i + (effectiveSlidesPerView - 1) : i;
            goToSlide(targetIndex);
          }, { passive: false });
          dotsContainer.appendChild(dot);
      }

      sliderContainer.appendChild(dotsContainer);

      if (sliderConfig.showArrows) {
        // Calcular posición vertical
        let verticalPosition = '50%';
        let verticalTransform = 'translateY(-50%)';
        
        if (sliderConfig.arrowVertical === 'top') {
          verticalPosition = `${sliderConfig.arrowDistance}rem`;
          verticalTransform = 'translateY(0)';
        } else if (sliderConfig.arrowVertical === 'bottom') {
          verticalPosition = 'auto';
          verticalTransform = 'translateY(0)';
        }

        const prevArrow = document.createElement('button');
        prevArrow.className = 'nav-arrows prev';
        prevArrow.setAttribute('aria-label', 'Previous slide');
        
        let arrowStyles = `
            position: absolute;
            ${sliderConfig.arrowVertical === 'top' ? 'top' : (sliderConfig.arrowVertical === 'bottom' ? 'bottom' : 'top')}: ${verticalPosition};
            ${sliderConfig.arrowVertical === 'bottom' ? 'bottom: ' + sliderConfig.arrowDistance + 'rem;' : ''}
            left: ${sliderConfig.arrowDistance}rem;
            transform: ${verticalTransform};
            width: ${sliderConfig.arrowSize}px;
            height: ${sliderConfig.arrowSize}px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 10;
            transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
            user-select: none;
            -webkit-tap-highlight-color: transparent;
            touch-action: manipulation;
            border: none;
        `;

        if (sliderConfig.showArrowBg) {
          const r = parseInt(sliderConfig.arrowBgColor.slice(1, 3), 16);
          const g = parseInt(sliderConfig.arrowBgColor.slice(3, 5), 16);
          const b = parseInt(sliderConfig.arrowBgColor.slice(5, 7), 16);
          arrowStyles += `
            background: rgba(${r}, ${g}, ${b}, ${sliderConfig.arrowBgOpacity});
            border-radius: 50%;
            backdrop-filter: blur(10px);
          `;
        } else {
          arrowStyles += `
            background: transparent;
          `;
        }
        
        prevArrow.style.cssText = arrowStyles;
        
        prevArrow.innerHTML = `<span style="
            width: ${Math.max(8, sliderConfig.arrowSize * 0.25)}px;
            height: ${Math.max(8, sliderConfig.arrowSize * 0.25)}px;
            border-top: 2px solid ${sliderConfig.arrowColor};
            border-right: 2px solid ${sliderConfig.arrowColor};
            transform: rotate(-135deg);
            margin-right: -2px;
            transition: all 0.3s ease;
        "></span>`;
        
        const nextArrow = document.createElement('button');
        nextArrow.className = 'nav-arrows next';
        nextArrow.setAttribute('aria-label', 'Next slide');
        nextArrow.style.cssText = arrowStyles.replace('left:', 'right:');
        
        nextArrow.innerHTML = `<span style="
            width: ${Math.max(8, sliderConfig.arrowSize * 0.25)}px;
            height: ${Math.max(8, sliderConfig.arrowSize * 0.25)}px;
            border-top: 2px solid ${sliderConfig.arrowColor};
            border-right: 2px solid ${sliderConfig.arrowColor};
            transform: rotate(45deg);
            margin-left: -2px;
            transition: all 0.3s ease;
        "></span>`;

        function addArrowEvents(arrow, callback) {
          arrow.addEventListener('click', callback);
          arrow.addEventListener('touchend', (e) => {
            e.preventDefault();
            callback();
          }, { passive: false });
          
          if (sliderConfig.showArrowBg) {
            arrow.addEventListener('mouseenter', function() {
              const r = parseInt(sliderConfig.arrowBgColor.slice(1, 3), 16);
              const g = parseInt(sliderConfig.arrowBgColor.slice(3, 5), 16);
              const b = parseInt(sliderConfig.arrowBgColor.slice(5, 7), 16);
              this.style.background = `rgba(${r}, ${g}, ${b}, ${Math.min(1, sliderConfig.arrowBgOpacity + 0.3)})`;
              this.style.transform = `${verticalTransform} scale(1.1)`;
            });
            
            arrow.addEventListener('mouseleave', function() {
              const r = parseInt(sliderConfig.arrowBgColor.slice(1, 3), 16);
              const g = parseInt(sliderConfig.arrowBgColor.slice(3, 5), 16);
              const b = parseInt(sliderConfig.arrowBgColor.slice(5, 7), 16);
              this.style.background = `rgba(${r}, ${g}, ${b}, ${sliderConfig.arrowBgOpacity})`;
              this.style.transform = verticalTransform;
            });
          }
        }

        addArrowEvents(prevArrow, () => {
          let newIndex = currentSlide - effectiveAdvanceStep;
          
          // Control del bucle infinito hacia atrás
          if (effectiveSlidesPerView > 1 && slidesCount >= effectiveSlidesPerView) {
            if (newIndex < (effectiveSlidesPerView - 1)) {
              // Saltar al final (antes de los clones finales)
              newIndex = slidesCount + (effectiveSlidesPerView - 1) - effectiveAdvanceStep;
              slidesWrapper.style.transition = 'none';
              goToSlideInstant(newIndex);
              setTimeout(() => {
                slidesWrapper.style.transition = `transform ${sliderConfig.transitionSpeed}s cubic-bezier(0.25, 0.8, 0.25, 1)`;
                goToSlide(newIndex);
              }, 50);
              return;
            }
          } else {
            if (newIndex < 0) {
              newIndex = slidesCount - 1;
            }
          }
          
          goToSlide(newIndex);
        });
        
        addArrowEvents(nextArrow, () => {
          let newIndex = currentSlide + effectiveAdvanceStep;
          
          // Control del bucle infinito hacia adelante  
          if (effectiveSlidesPerView > 1 && slidesCount >= effectiveSlidesPerView) {
            if (newIndex >= slidesCount + (effectiveSlidesPerView - 1)) {
              // Saltar al inicio (después de los clones iniciales)
              newIndex = (effectiveSlidesPerView - 1) + effectiveAdvanceStep;
              slidesWrapper.style.transition = 'none';
              goToSlideInstant(newIndex);
              setTimeout(() => {
                slidesWrapper.style.transition = `transform ${sliderConfig.transitionSpeed}s cubic-bezier(0.25, 0.8, 0.25, 1)`;
                goToSlide(newIndex);
              }, 50);
              return;
            }
          } else {
            if (newIndex >= slidesCount) {
              newIndex = 0;
            }
          }
          
          goToSlide(newIndex);
        });

        sliderContainer.appendChild(prevArrow);
        sliderContainer.appendChild(nextArrow);
      }
      
      if (sliderConfig.enableDrag) {
        setupDragging();
      }
      
      sliderContainer.setAttribute('data-slider-initialized', 'true');
      
      goToSlide(currentSlide);

      function goToSlideInstant(index) {
        currentSlide = index;
        updateSlidePosition();
        updateVisualStates();
      }

      function goToSlide(index) {
          currentSlide = index;
          updateSlidePosition();
          updateVisualStates();
      }
      
      function updateSlidePosition() {
        const containerWidth = sliderContainer.offsetWidth;
        const slideWidthPx = (containerWidth - (sliderConfig.slideGap * (effectiveSlidesPerView - 1))) / effectiveSlidesPerView;
        const totalSlideWidth = slideWidthPx + sliderConfig.slideGap;
        
        const translateValue = -(currentSlide * totalSlideWidth);
        slidesWrapper.style.transform = `translateX(${translateValue}px)`;
      }
      
      function updateVisualStates() {
        const allSlides = slidesWrapper.children;
        
        // Actualizar estilos de slides
        for (let i = 0; i < allSlides.length; i++) {
            const isVisible = effectiveSlidesPerView === 1 ? 
                i === currentSlide : 
                i >= currentSlide && i < (currentSlide + effectiveSlidesPerView);
            
            if (isVisible) {
                allSlides[i].style.opacity = '1';
                allSlides[i].style.transform = `scale(${sliderConfig.activeScale})`;
            } else {
                allSlides[i].style.opacity = sliderConfig.inactiveOpacity.toString();
                allSlides[i].style.transform = 'scale(1)';
            }
        }

        // Actualizar dots (basado en slides originales)
        const dots = dotsContainer.children;
        let activeDotIndex = currentSlide;
        
        if (effectiveSlidesPerView > 1 && slidesCount >= effectiveSlidesPerView) {
          // Compensar por los clones del inicio
          activeDotIndex = (currentSlide - (effectiveSlidesPerView - 1) + slidesCount) % slidesCount;
        }
        
        for (let i = 0; i < dots.length; i++) {
            if (i === activeDotIndex) {
                dots[i].style.width = '1.5rem';
                dots[i].style.backgroundColor = sliderConfig.dotColor;
            } else {
                dots[i].style.width = '0.375rem';
                dots[i].style.backgroundColor = sliderConfig.inactiveDotColor;
            }
        }
      }
      
      function setupDragging() {
        function getX(event) {
          return event.type.includes('mouse') ? event.clientX : event.touches[0].clientX;
        }

        function getY(event) {
          return event.type.includes('mouse') ? event.clientY : event.touches[0].clientY;
        }

        function handleStart(event) {
          if (!sliderConfig.enableDrag) return;
          
          isDragging = true;
          hasDetectedDirection = false;
          allowHorizontalDrag = false;
          startX = getX(event);
          startY = getY(event);
          currentX = startX;
          currentY = startY;
          
          const transform = getComputedStyle(slidesWrapper).transform;
          if (transform !== 'none') {
            const matrix = new DOMMatrixReadOnly(transform);
            initialTransform = matrix.m41;
          } else {
            initialTransform = 0;
          }
          
          slidesWrapper.style.transition = 'none';
          
          if (autoplayInterval) {
            clearInterval(autoplayInterval);
          }
        }

        function handleMove(event) {
          if (!isDragging) return;
          
          currentX = getX(event);
          currentY = getY(event);
          
          const deltaX = Math.abs(currentX - startX);
          const deltaY = Math.abs(currentY - startY);
          
          if (!hasDetectedDirection && (deltaX > 8 || deltaY > 8)) {
            hasDetectedDirection = true;
            
            if (deltaX > deltaY * 1.5) {
              allowHorizontalDrag = true;
              sliderContainer.style.cursor = 'grabbing';
              event.preventDefault();
            } else {
              isDragging = false;
              slidesWrapper.style.transition = `transform ${sliderConfig.transitionSpeed}s cubic-bezier(0.25, 0.8, 0.25, 1)`;
              sliderContainer.style.cursor = sliderConfig.enableDrag ? 'grab' : 'default';
              return;
            }
          }
          
          if (allowHorizontalDrag) {
            const deltaXMove = currentX - startX;
            const newTransform = initialTransform + deltaXMove;
            slidesWrapper.style.transform = `translateX(${newTransform}px)`;
            event.preventDefault();
          }
        }

        function handleEnd(event) {
          if (!isDragging || !allowHorizontalDrag) {
            isDragging = false;
            return;
          }
          
          isDragging = false;
          sliderContainer.style.cursor = sliderConfig.enableDrag ? 'grab' : 'default';
          slidesWrapper.style.transition = `transform ${sliderConfig.transitionSpeed}s cubic-bezier(0.25, 0.8, 0.25, 1)`;
          
          const deltaX = currentX - startX;
          const sensitivity = Math.max(20, 100 - sliderConfig.dragSensitivity);
          const threshold = sliderContainer.offsetWidth / (100 / sensitivity);
          
          let newIndex = currentSlide;
          
          if (Math.abs(deltaX) > threshold) {
            if (deltaX < 0) {
              // Avanzar
              newIndex = currentSlide + effectiveAdvanceStep;
              if (effectiveSlidesPerView > 1 && slidesCount >= effectiveSlidesPerView) {
                if (newIndex >= slidesCount + (effectiveSlidesPerView - 1)) {
                  newIndex = (effectiveSlidesPerView - 1) + effectiveAdvanceStep;
                  slidesWrapper.style.transition = 'none';
                  goToSlideInstant(newIndex);
                  setTimeout(() => {
                    slidesWrapper.style.transition = `transform ${sliderConfig.transitionSpeed}s cubic-bezier(0.25, 0.8, 0.25, 1)`;
                    goToSlide(newIndex);
                  }, 50);
                  if (sliderConfig.enableAutoplay) startAutoplay();
                  return;
                }
              } else {
                if (newIndex >= slidesCount) newIndex = 0;
              }
            } else {
              // Retroceder
              newIndex = currentSlide - effectiveAdvanceStep;
              if (effectiveSlidesPerView > 1 && slidesCount >= effectiveSlidesPerView) {
                if (newIndex < (effectiveSlidesPerView - 1)) {
                  newIndex = slidesCount + (effectiveSlidesPerView - 1) - effectiveAdvanceStep;
                  slidesWrapper.style.transition = 'none';
                  goToSlideInstant(newIndex);
                  setTimeout(() => {
                    slidesWrapper.style.transition = `transform ${sliderConfig.transitionSpeed}s cubic-bezier(0.25, 0.8, 0.25, 1)`;
                    goToSlide(newIndex);
                  }, 50);
                  if (sliderConfig.enableAutoplay) startAutoplay();
                  return;
                }
              } else {
                if (newIndex < 0) newIndex = slidesCount - 1;
              }
            }
          }
          
          goToSlide(newIndex);
          
          if (sliderConfig.enableAutoplay) {
            startAutoplay();
          }
        }

        sliderContainer.addEventListener('mousedown', handleStart);
        sliderContainer.addEventListener('touchstart', handleStart, { passive: true });
        document.addEventListener('mousemove', handleMove);
        document.addEventListener('touchmove', handleMove, { passive: false });
        document.addEventListener('mouseup', handleEnd);
        document.addEventListener('touchend', handleEnd);
      }

      if (sliderConfig.enableAutoplay) {
          startAutoplay();
      }
      
      function startAutoplay() {
          if (autoplayInterval) {
              clearInterval(autoplayInterval);
          }
          
          autoplayInterval = setInterval(() => {
              let nextSlide = currentSlide + effectiveAdvanceStep;
              
              // Control del bucle infinito en autoplay
              if (effectiveSlidesPerView > 1 && slidesCount >= effectiveSlidesPerView) {
                if (nextSlide >= slidesCount + (effectiveSlidesPerView - 1)) {
                  nextSlide = (effectiveSlidesPerView - 1) + effectiveAdvanceStep;
                  slidesWrapper.style.transition = 'none';
                  goToSlideInstant(nextSlide);
                  setTimeout(() => {
                    slidesWrapper.style.transition = `transform ${sliderConfig.transitionSpeed}s cubic-bezier(0.25, 0.8, 0.25, 1)`;
                    goToSlide(nextSlide);
                  }, 50);
                  return;
                }
              } else {
                if (nextSlide >= slidesCount) {
                  nextSlide = 0;
                }
              }
              
              goToSlide(nextSlide);
          }, sliderConfig.autoplayInterval * 1000);
      }
      
      sliderContainer.addEventListener('mouseenter', () => {
          if (autoplayInterval) {
              clearInterval(autoplayInterval);
          }
      });
      
      sliderContainer.addEventListener('mouseleave', () => {
          if (sliderConfig.enableAutoplay && !isDragging) {
              startAutoplay();
          }
      });
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initSlider);
  } else {
    initSlider();
  }

  window.addEventListener('load', initSlider);
  
  const observer = new MutationObserver((mutations) => {
    let shouldInit = false;
    
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList') {
        mutation.addedNodes.forEach(node => {
          if (node.nodeType === 1) {
            if (node.hasAttribute && node.hasAttribute('data-slider')) {
              shouldInit = true;
            } else if (node.querySelectorAll) {
              const sliderElements = node.querySelectorAll('[data-slider]');
              if (sliderElements.length > 0) {
                shouldInit = true;
              }
            }
          }
        });
      } else if (mutation.type === 'attributes' && mutation.attributeName === 'data-slider') {
        shouldInit = true;
      }
    });
    
    if (shouldInit) {
      setTimeout(initSlider, 100);
    }
  });
  
  observer.observe(document.body, { 
    childList: true, 
    subtree: true,
    attributes: true,
    attributeFilter: ['data-slider', 'data-slide']
  });
})();
(function() {
  // Base class with shared functionality
  class VerticalFlowBase {
    constructor(container, config) {
      this.container = container;
      this.config = { ...config };
      this.track = null;
      this.animationId = null;
      this.currentY = 0;
      this.items = [];
      this.clones = [];
      this.containerHeight = 0;
      this.contentHeight = 0;
      this.totalContentHeight = 0;
      this.isRunning = false;
      this.isVisible = true;
      this.lastMeasureTime = 0;
      this.observers = [];
      this.timers = [];
      this.isInitialized = false;
      
      this.init();
    }
    
    init() {
      this.setupIntersectionObserver();
      this.setupResizeObserver();
      this.createTrack();
    }
    
    setupIntersectionObserver() {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          this.isVisible = entry.isIntersecting;
          if (!this.isVisible) {
            this.pause();
          } else if (this.isInitialized) {
            this.resume();
          }
        });
      }, {
        threshold: 0.1,
        rootMargin: '50px'
      });
      
      observer.observe(this.container);
      this.observers.push(observer);
    }
    
    setupResizeObserver() {
      let lastHeight = this.container.offsetHeight;
      
      const handleResize = this.debounce(() => {
        const currentHeight = this.container.offsetHeight;
        if (Math.abs(currentHeight - lastHeight) < 10) return;
        
        lastHeight = currentHeight;
        
        if (this.isVisible && this.isInitialized) {
          this.measureDimensions();
          this.createSeamlessLoop();
        }
      }, 250);
      
      if (window.ResizeObserver) {
        const observer = new ResizeObserver(handleResize);
        observer.observe(this.container);
        this.observers.push(observer);
      } else {
        window.addEventListener('resize', handleResize, { passive: true });
        this.legacyResizeHandler = handleResize;
      }
    }
    
    debounce(func, wait) {
      let timeout;
      return (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
        this.timers.push(timeout);
      };
    }
    
    createTrack() {
      this.track = this.container.querySelector('.verticalflow-track');
      if (!this.track) return;
      
      this.track.innerHTML = '';
      this.items = [];
      this.clones = [];
      this.isInitialized = false;
      
      const validImages = this.config.images.filter(url => url && url.trim() !== '');
      
      validImages.forEach(url => {
        const item = this.createItem(url);
        this.items.push(item);
        this.track.appendChild(item);
      });
      
      this.track.style.willChange = 'transform';
      this.track.style.transform = 'translate3d(0, 0, 0)';
      this.track.style.backfaceVisibility = 'hidden';
      
      this.preloadImages().then(() => {
        this.measureDimensions();
        this.createSeamlessLoop();
        this.applyStyles();
        this.isInitialized = true;
        this.start();
      });
    }
    
    preloadImages() {
      const imageElements = this.items.map(item => item.querySelector('img')).filter(img => img);
      
      const promises = imageElements.map(img => {
        return new Promise((resolve) => {
          if (img.complete && img.naturalWidth > 0) {
            resolve();
            return;
          }
          
          const timeout = setTimeout(() => {
            cleanup();
            resolve();
          }, 3000);
          
          const cleanup = () => {
            clearTimeout(timeout);
            img.removeEventListener('load', handleLoad);
            img.removeEventListener('error', handleError);
          };
          
          const handleLoad = () => {
            cleanup();
            resolve();
          };
          
          const handleError = () => {
            cleanup();
            resolve();
          };
          
          img.addEventListener('load', handleLoad);
          img.addEventListener('error', handleError);
        });
      });
      
      return Promise.all(promises);
    }
    
    createItem(url) {
      const item = document.createElement('div');
      item.className = 'verticalflow-item';
      item.style.borderRadius = `${this.config.borderRadius}px`;
      item.style.position = 'relative';
      
      const img = document.createElement('img');
      img.src = url;
      img.alt = '';
      img.style.pointerEvents = 'none';
      img.loading = 'lazy';
      
      img.onerror = function() {
        this.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300' viewBox='0 0 400 300'%3E%3Crect width='400' height='300' fill='%23555555'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='24' fill='%23ffffff'%3EImage not found%3C/text%3E%3C/svg%3E";
      };
      
      item.appendChild(img);
      
      if (this.config.overlayEnabled) {
        const overlay = document.createElement('div');
        overlay.className = 'verticalflow-overlay';
        overlay.style.cssText = `
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background-color: ${this.config.overlayColor};
          opacity: ${this.config.overlayOpacity};
          border-radius: ${this.config.borderRadius}px;
          pointer-events: none;
        `;
        
        item.appendChild(overlay);
      }
      
      return item;
    }
    
    measureDimensions() {
      const now = Date.now();
      if (now - this.lastMeasureTime < 50) return;
      this.lastMeasureTime = now;
      
      this.containerHeight = this.container.offsetHeight;
      if (this.containerHeight === 0) return;
      
      this.updateItemDimensions();
      
      const sampleItem = this.items[0];
      if (sampleItem && sampleItem.offsetHeight > 0) {
        this.contentHeight = (sampleItem.offsetHeight + this.config.imageGap) * this.items.length - this.config.imageGap;
      } else {
        this.contentHeight = (this.config.imageHeight + this.config.imageGap) * this.items.length - this.config.imageGap;
      }
    }
    
    updateItemDimensions() {
      const containerWidth = this.container.offsetWidth;
      const itemHeight = this.config.imageHeight;
      const gapSize = this.config.imageGap;
      
      let itemWidth;
      if (this.config.imageWidthMode === 'custom') {
        itemWidth = Math.floor((containerWidth * this.config.customImageWidth) / 100);
      } else {
        itemWidth = containerWidth;
      }
      
      const allItems = [...this.items, ...this.clones];
      allItems.forEach(item => {
        item.style.cssText += `
          width: ${itemWidth}px;
          height: ${itemHeight}px;
          margin-bottom: ${gapSize}px;
          flex-shrink: 0;
        `;
        
        switch (this.config.horizontalAlignment) {
          case 'left':
            item.style.marginLeft = '0';
            item.style.marginRight = 'auto';
            break;
          case 'right':
            item.style.marginLeft = 'auto';
            item.style.marginRight = '0';
            break;
          default:
            item.style.marginLeft = 'auto';
            item.style.marginRight = 'auto';
            break;
        }
      });
    }
    
    createSeamlessLoop() {
      if (this.contentHeight === 0 || this.containerHeight === 0) return;
      
      this.clones.forEach(clone => clone.remove());
      this.clones.length = 0;
      
      const bufferMultiplier = 3;
      const requiredHeight = this.containerHeight * bufferMultiplier;
      const clonesNeeded = Math.max(1, Math.ceil(requiredHeight / this.contentHeight));
      
      const fragment = document.createDocumentFragment();
      
      for (let i = 0; i < clonesNeeded; i++) {
        this.items.forEach(item => {
          const clone = item.cloneNode(true);
          clone.classList.add('verticalflow-clone');
          clone.style.willChange = 'transform';
          fragment.appendChild(clone);
          this.clones.push(clone);
        });
      }
      
      this.track.appendChild(fragment);
      
      this.updateItemDimensions();
      this.totalContentHeight = this.contentHeight * (1 + clonesNeeded);
      this.initializeSeamlessPosition();
    }
    
    initializeSeamlessPosition() {
      this.currentY = 0;
      this.track.style.transform = `translate3d(0, ${this.currentY}px, 0)`;
    }
    
    applyStyles() {
      const verticalflowContainer = this.container.querySelector('.verticalflow-container');
      const topFade = this.container.querySelector('.verticalflow-edge-fade.top');
      const bottomFade = this.container.querySelector('.verticalflow-edge-fade.bottom');
      
      if (!verticalflowContainer || !topFade || !bottomFade) return;
      
      const fadeColor = this.config.blurTheme === 'dark'
        ? `rgba(0, 0, 0, ${this.config.fadeOpacity})`
        : `rgba(255, 255, 255, ${this.config.fadeOpacity})`;
      
      const fadeOutColor = this.config.blurTheme === 'dark'
        ? 'rgba(0, 0, 0, 0)'
        : 'rgba(255, 255, 255, 0)';
      
      topFade.style.cssText += `
        height: ${this.config.fadeHeight}%;
        background: linear-gradient(to bottom, ${fadeColor} 0%, ${fadeOutColor} 100%);
      `;
      
      bottomFade.style.cssText += `
        height: ${this.config.fadeHeight}%;
        background: linear-gradient(to top, ${fadeColor} 0%, ${fadeOutColor} 100%);
      `;
    }
    
    start() {
      if (this.isRunning || !this.isVisible || !this.isInitialized) return;
      this.isRunning = true;
      this.animate();
    }
    
    pause() {
      this.isRunning = false;
      if (this.animationId) {
        cancelAnimationFrame(this.animationId);
        this.animationId = null;
      }
    }
    
    resume() {
      if (!this.isRunning && this.isVisible && this.isInitialized) {
        this.start();
      }
    }
    
    animate() {
      if (!this.isRunning || !this.isVisible || !this.isInitialized) return;
      
      const isMobile = window.innerWidth < 768;
      const baseDuration = this.config.speed;
      const deviceFactor = isMobile ? this.config.mobileSpeed : 1;
      
      const speed = (this.containerHeight / (baseDuration * deviceFactor * 60)) * 1.2;
      
      if (this.config.direction === 'down') {
        this.currentY += speed;
        
        if (this.currentY >= this.contentHeight + this.config.imageGap) {
          this.currentY = 0;
        }
      } else {
        this.currentY -= speed;
        
        if (Math.abs(this.currentY) >= this.contentHeight + this.config.imageGap) {
          this.currentY = 0;
        }
      }
      
      this.track.style.transform = `translate3d(0, ${this.currentY}px, 0)`;
      
      this.animationId = requestAnimationFrame(() => this.animate());
    }
    
    updateConfig(newConfig) {
      Object.assign(this.config, newConfig);
      this.createTrack();
    }
    
    destroy() {
      this.pause();
      
      this.clones.forEach(clone => clone.remove());
      this.clones.length = 0;
      
      this.observers.forEach(observer => {
        if (observer.disconnect) observer.disconnect();
      });
      this.observers.length = 0;
      
      this.timers.forEach(timer => clearTimeout(timer));
      this.timers.length = 0;
      
      if (this.legacyResizeHandler) {
        window.removeEventListener('resize', this.legacyResizeHandler);
        this.legacyResizeHandler = null;
      }
    }
  }

  class VerticalFlowCarousel {
    constructor() {
      this.config = {
        speed: 10,
        direction: "up",
        imageGap: 20,
        borderRadius: 8,
        blurTheme: "dark",
        fadeHeight: 15,
        fadeOpacity: 0.95,
        overlayEnabled: false,
        overlayColor: "#000000",
        overlayOpacity: 0.3,
        imageWidthMode: "full",
        customImageWidth: 80,
        imageHeight: 330,
        horizontalAlignment: "center",
        mobileSpeed: 0.5,
        images: ["https://cdn.dribbble.com/userupload/12792975/file/original-b06c2e06524e4cbb84aea0a28bcf400d.png?format=webp&resize=640x480&vertical=center","https://cdn.dribbble.com/userupload/18430775/file/original-6b0773a25faafe95fc6a1fb3ac78ec3d.png?format=webp&resize=640x480&vertical=center","https://cdn.dribbble.com/userupload/17940035/file/original-ad3f506ad812023e1cf9250cd41a9e32.png?format=webp&resize=640x480&vertical=center","https://cdn.dribbble.com/userupload/11401300/file/original-1540266e43d15dacc5ee91e10d300f85.png?format=webp&resize=640x480&vertical=center","https://cdn.dribbble.com/userupload/6236860/file/original-a7498f67f05a5e4cfd9d6a9a9dad0b72.jpg?format=webp&resize=640x480&vertical=center"]
      };
      
      this.carousels = [];
      this.init();
    }
    
    init() {
      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => this.findCarousels());
      } else {
        this.findCarousels();
      }
    }
    
    findCarousels() {
      document.querySelectorAll('[data-verticalflow]').forEach(container => {
        const originalPosition = window.getComputedStyle(container).position;
        
        if (originalPosition === 'static') {
          container.style.position = 'relative';
        }
        
        // Add touch-action for better mobile performance
        container.style.touchAction = 'pan-x pinch-zoom';
        
        this.createCarousel(container);
      });
    }
    
    createCarousel(container) {
      if (!container.querySelector('.verticalflow-container')) {
        const verticalflowWrapper = document.createElement('div');
        verticalflowWrapper.className = 'verticalflow-wrapper';
        verticalflowWrapper.style.cssText = `
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          z-index: 0;
          pointer-events: none;
          overflow: hidden;
        `;
        
        const carouselHTML = `
          <div class="verticalflow-container">
            <div class="verticalflow-lane">
              <div class="verticalflow-track"></div>
            </div>
            <div class="verticalflow-edge-fade top"></div>
            <div class="verticalflow-edge-fade bottom"></div>
          </div>
        `;
        
        verticalflowWrapper.innerHTML = carouselHTML;
        
        if (container.firstChild) {
          container.insertBefore(verticalflowWrapper, container.firstChild);
        } else {
          container.appendChild(verticalflowWrapper);
        }
        
        this.applyStyles();
        
        const carouselInstance = new VerticalFlowInstance(verticalflowWrapper, this.config);
        this.carousels.push(carouselInstance);
        
        container._verticalflowInstance = carouselInstance;
      }
    }
    
    applyStyles() {
      if (!document.getElementById('verticalflow-styles')) {
        const styleEl = document.createElement('style');
        styleEl.id = 'verticalflow-styles';
        
        const fadeColor = this.config.blurTheme === 'dark'
          ? `rgba(0, 0, 0, ${this.config.fadeOpacity})`
          : `rgba(255, 255, 255, ${this.config.fadeOpacity})`;
        
        const fadeOutColor = this.config.blurTheme === 'dark'
          ? 'rgba(0, 0, 0, 0)'
          : 'rgba(255, 255, 255, 0)';
        
        const css = `
          .verticalflow-wrapper {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 0;
            pointer-events: none;
            overflow: hidden;
          }
          
          .verticalflow-container {
            position: relative;
            width: 100%;
            height: 100%;
            overflow: hidden;
            border-radius: ${this.config.borderRadius}px;
            margin: 0;
            padding: 0;
          }
          
          .verticalflow-lane {
            position: relative;
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            justify-content: center;
            overflow: hidden;
            margin: 0;
            padding: 0;
          }
          
          .verticalflow-track {
            display: flex;
            flex-direction: column;
            width: 100%;
            min-height: 100%;
            will-change: transform;
            backface-visibility: hidden;
            perspective: 1000px;
            transform: translate3d(0, 0, 0);
            margin: 0;
            padding: 0;
          }
          
          .verticalflow-item {
            flex: 0 0 auto;
            overflow: hidden;
            border-radius: ${this.config.borderRadius}px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
            position: relative;
            margin: 0 auto ${this.config.imageGap}px auto;
            padding: 0;
            pointer-events: none;
            will-change: transform;
            flex-shrink: 0;
            height: ${this.config.imageHeight}px;
          }
          
          .verticalflow-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
            pointer-events: none;
          }
          
          .verticalflow-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: ${this.config.overlayColor};
            opacity: ${this.config.overlayOpacity};
            border-radius: ${this.config.borderRadius}px;
            pointer-events: none;
          }
          
          .verticalflow-edge-fade {
            position: absolute;
            left: 0;
            width: 100%;
            height: ${this.config.fadeHeight}%;
            z-index: 3;
            pointer-events: none;
          }
          
          .verticalflow-edge-fade.top {
            top: 0;
            background: linear-gradient(to bottom, ${fadeColor} 0%, ${fadeOutColor} 100%);
          }
          
          .verticalflow-edge-fade.bottom {
            bottom: 0;
            background: linear-gradient(to top, ${fadeColor} 0%, ${fadeOutColor} 100%);
          }
        `;
        
        styleEl.textContent = css;
        document.head.appendChild(styleEl);
      }
    }
  }
  
  // Production class extends base with same optimizations
  class VerticalFlowInstance extends VerticalFlowBase {
    constructor(wrapper, config) {
      super(wrapper, config);
    }
    
    applyStyles() {
      super.applyStyles();
      
      // Apply width and alignment styles to items
      this.updateItemDimensions();
    }
  }
  
  new VerticalFlowCarousel();
})();
Submit any design task you need. Landing pages, product visuals, brand assets, and more.
$2,995/month
Unlimited design requests
One active task at a time
Delivered in a few business days
Source files included
Cancel or pause anytime
Join today
FAQs

Frequently Asked Questions

Emailhello@loomia.com
Get in touch

Lorem ipsum dolor ist amte, consectetuer adipiscing eilt. Aenean commodo ligula egget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quak felis, ultricies nec, pellentesque eu, pretium quid, sem.

Lorem ipsum dolor ist amte, consectetuer adipiscing eilt. Aenean commodo ligula egget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quak felis, ultricies nec, pellentesque eu, pretium quid, sem.

Lorem ipsum dolor ist amte, consectetuer adipiscing eilt. Aenean commodo ligula egget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quak felis, ultricies nec, pellentesque eu, pretium quid, sem.

Lorem ipsum dolor ist amte, consectetuer adipiscing eilt. Aenean commodo ligula egget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quak felis, ultricies nec, pellentesque eu, pretium quid, sem.

Lorem ipsum dolor ist amte, consectetuer adipiscing eilt. Aenean commodo ligula egget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quak felis, ultricies nec, pellentesque eu, pretium quid, sem.

Edit me. I am an alert.

Yayasan Peduli

Tuntaskan Fidyah: Bantu Menunaikan Kewajiban Ibadah

Bergabung untuk membantu menunaikan fidyah bagi saudara-saudara kita yang membutuhkan. Setiap bantuan Anda akan sangat berarti.

Rp 150.000.000terkumpul
target Rp 300.000.000
  • 1.256 Donatur
  • Rp 150.000.000 Terkumpul
  • 45 Hari

Tentang Campaign

Fidyah adalah kompensasi untuk makanan yang diberikan kepada orang lain sebagai pengganti puasa yang batal. Melalui program ini, kami menyalurkan fidyah secara langsung ke keluarga prasejahtera. Dana yang terkumpul akan digunakan untuk membeli bahan makanan pokok dan didistribusikan oleh relawan setempat.

Dampak Bantuan

  • Paket makanan untuk keluarga terdampak
  • Distribusi oleh relawan terpercaya
  • Pendampingan penerima manfaat

Pertanyaan yang Sering Diajukan