(function ($) {
    "use strict";

    // ── Utilities ──────────────────────────────────────────────────────────────
    const lerp = (a, b, t) => a + (b - a) * t;
    const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
    const mapRange = (v, a, b, c, d) => c + ((v - a) / (b - a)) * (d - c);

    let rafId = null;

    // ── Animation Engine Init ──────────────────────────────────────────────────
    const initAnimations = () => {
        // Unified RAF loop — drives all scroll effects independent of specific smooth scroll libs
        const tick = () => {
            runEffects();
            rafId = requestAnimationFrame(tick);
        };

        if (!rafId) {
            rafId = requestAnimationFrame(tick);
        }
    };

    // ── Scroll Effect Engine ───────────────────────────────────────────────────
    const LERP_FACTOR = 0.08;          // global smoothness (lower = smoother)
    let elements = [];                 // registered effect nodes
    let scrollY = 0;                   // raw scroll position
    let smoothScrollY = 0;             // lerped scroll position
    let viewH = window.innerHeight;
    let resizeTimer;

    const initScrollEffects = () => {
        const nodes = document.querySelectorAll('[data-lenis-effect]');
        if (!nodes.length) return;

        elements = [];

        nodes.forEach(el => {
            const rect = el.getBoundingClientRect();
            elements.push({
                el,
                effect: el.dataset.lenisEffect,
                speed: parseFloat(el.dataset.lenisSpeed || 1),
                origin: el.dataset.lenisOrigin || 'center', // top | center | bottom
                lerpFactor: parseFloat(el.dataset.lenisLerp || LERP_FACTOR),
                // current interpolated values
                curX: 0, curY: 0, curScale: 1,
                curOpacity: 1, curRotate: 0, curBlur: 0,
                // target values updated every frame
                tgtX: 0, tgtY: 0, tgtScale: 1,
                tgtOpacity: 1, tgtRotate: 0, tgtBlur: 0,
                // cached layout (refreshed on resize)
                top: rect.top + window.scrollY,
                height: rect.height,
                // IntersectionObserver visibility flag
                visible: true,
            });

            el.style.willChange = 'transform, opacity, filter';
        });

        // Stop animating elements that are completely off-screen
        const io = new IntersectionObserver(
            (entries) => entries.forEach(e => {
                const item = elements.find(d => d.el === e.target);
                if (item) item.visible = e.isIntersecting;
            }),
            { rootMargin: '20% 0px 20% 0px' }
        );
        elements.forEach(d => io.observe(d.el));

        // Debounced resize handler
        window.addEventListener('resize', () => {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(refreshRects, 200);
        });
    };

    const refreshRects = () => {
        viewH = window.innerHeight;
        elements.forEach(d => {
            const rect = d.el.getBoundingClientRect();
            d.top = rect.top + window.scrollY;
            d.height = rect.height;
        });
    };

    // ── Per-frame computation ──────────────────────────────────────────────────
    const runEffects = () => {
        scrollY = window.scrollY || 0; // Capture native scroll position each frame
        
        if (!elements.length) return;

        // Lerp the scroll position itself for an extra layer of silk
        smoothScrollY = lerp(smoothScrollY, scrollY, 0.12);

        elements.forEach(d => {
            if (!d.visible) return;

            // Anchor point inside the element
            const anchor = d.origin === 'top' ? d.top :
                d.origin === 'bottom' ? d.top + d.height :
                    d.top + d.height * 0.5;

            // Progress: 0 when anchor is at bottom of viewport, 1 at top
            const progress = clamp(
                mapRange(smoothScrollY + viewH * 0.5, anchor - viewH * 0.6, anchor + viewH * 0.6, -1, 1),
                -1, 1
            );

            // ── Effects ──────────────────────────────────────────────────────
            switch (d.effect) {

                case 'parallax_y':
                    d.tgtY = progress * d.speed * 80;
                    break;

                case 'parallax_x':
                    d.tgtX = progress * d.speed * 60;
                    break;

                case 'scale':
                    d.tgtScale = clamp(1 - Math.abs(progress) * 0.25 * d.speed, 0.75, 1.1);
                    break;

                case 'scale_up':
                    d.tgtScale = clamp(mapRange(progress, -1, 0, 0.8, 1), 0.8, 1);
                    break;

                case 'fade':
                    d.tgtOpacity = clamp(1 - Math.abs(progress) * 1.2 * d.speed, 0, 1);
                    break;

                case 'fade_in':
                    d.tgtOpacity = clamp(mapRange(progress, -1, -0.3, 0, 1), 0, 1);
                    break;

                case 'rotate':
                    d.tgtRotate = progress * 18 * d.speed;
                    break;

                case 'flip_x':
                    d.tgtRotate = clamp(progress * 45 * d.speed, -70, 70);
                    break;

                case 'flip_y':
                    d.tgtX = clamp(progress * 45 * d.speed, -70, 70);  // rotateY encoded as tgtX
                    break;

                case 'skew':
                    d.tgtRotate = progress * 10 * d.speed;  // skewY encoded
                    break;

                case 'blur':
                    d.tgtBlur = Math.abs(progress) * 10 * d.speed;
                    break;

                case 'depth':
                    d.tgtScale = clamp(1 - Math.abs(progress) * 0.3 * d.speed, 0.6, 1);
                    d.tgtOpacity = clamp(1 - Math.abs(progress) * 0.5, 0.3, 1);
                    break;

                case 'float':
                    // Gentle hover — position-independent, uses real scrollY wave
                    d.tgtY = Math.sin(scrollY * 0.003 + d.top * 0.001) * 14 * d.speed;
                    break;

                case 'arc':
                    d.tgtX = progress * progress * 40 * d.speed;
                    d.tgtY = progress * 40 * d.speed;
                    break;

                case 'zoom_rotate':
                    d.tgtScale = clamp(1 - Math.abs(progress) * 0.35 * d.speed, 0.6, 1);
                    d.tgtRotate = progress * 12 * d.speed;
                    break;

                case 'slide_left':
                    d.tgtX = clamp(mapRange(progress, -1, -0.2, -80, 0), -80, 0) * d.speed;
                    d.tgtOpacity = clamp(mapRange(progress, -1, -0.3, 0, 1), 0, 1);
                    break;

                case 'slide_right':
                    d.tgtX = clamp(mapRange(progress, -1, -0.2, 80, 0), 0, 80) * d.speed;
                    d.tgtOpacity = clamp(mapRange(progress, -1, -0.3, 0, 1), 0, 1);
                    break;

                case 'slide_up':
                    d.tgtY = clamp(mapRange(progress, -1, -0.2, 60, 0), 0, 60) * d.speed;
                    d.tgtOpacity = clamp(mapRange(progress, -1, -0.3, 0, 1), 0, 1);
                    break;

                case 'tilt':
                    d.tgtX = progress * 6 * d.speed;   // subtle skewX
                    d.tgtY = progress * 20 * d.speed;
                    break;
            }

            // ── Lerp current → target (per-element lerp factor) ──────────────
            const lf = d.lerpFactor;
            d.curX = lerp(d.curX, d.tgtX, lf);
            d.curY = lerp(d.curY, d.tgtY, lf);
            d.curScale = lerp(d.curScale, d.tgtScale, lf);
            d.curOpacity = lerp(d.curOpacity, d.tgtOpacity, lf);
            d.curRotate = lerp(d.curRotate, d.tgtRotate, lf);
            d.curBlur = lerp(d.curBlur, d.tgtBlur, lf);

            // ── Compose transform string ──────────────────────────────────────
            let t = '';
            switch (d.effect) {
                case 'parallax_y':
                case 'slide_up':
                case 'fade_in':
                case 'float':
                case 'depth':
                    t = `translate3d(0, ${d.curY.toFixed(2)}px, 0)`;
                    break;
                case 'parallax_x':
                case 'slide_left':
                case 'slide_right':
                    t = `translate3d(${d.curX.toFixed(2)}px, 0, 0)`;
                    break;
                case 'scale':
                case 'scale_up':
                    t = `scale(${d.curScale.toFixed(4)})`;
                    break;
                case 'rotate':
                case 'zoom_rotate':
                    t = `scale(${d.curScale.toFixed(4)}) rotate(${d.curRotate.toFixed(3)}deg)`;
                    break;
                case 'flip_x':
                    t = `perspective(1200px) rotateX(${d.curRotate.toFixed(3)}deg)`;
                    break;
                case 'flip_y':
                    t = `perspective(1200px) rotateY(${d.curX.toFixed(3)}deg)`;
                    break;
                case 'skew':
                    t = `skewY(${d.curRotate.toFixed(3)}deg)`;
                    break;
                case 'arc':
                case 'tilt':
                    t = `translate3d(${d.curX.toFixed(2)}px, ${d.curY.toFixed(2)}px, 0)`;
                    break;
                default:
                    if (d.curY !== 0 || d.curX !== 0)
                        t = `translate3d(${d.curX.toFixed(2)}px, ${d.curY.toFixed(2)}px, 0)`;
            }

            if (t) d.el.style.transform = t;
            if (d.effect === 'fade' || d.effect === 'fade_in' ||
                d.effect === 'depth' || d.effect === 'slide_left' ||
                d.effect === 'slide_right' || d.effect === 'slide_up')
                d.el.style.opacity = d.curOpacity.toFixed(4);
            if (d.effect === 'blur')
                d.el.style.filter = `blur(${d.curBlur.toFixed(2)}px)`;
        });
    };

    // ── Elementor Boot ─────────────────────────────────────────────────────────
    $(window).on('elementor/frontend/init', function () {
        initAnimations();

        elementorFrontend.hooks.addAction('frontend/element_ready/global', function () {
            // Debounce multiple rapid calls during Elementor's own init burst
            clearTimeout(window._sifencyInitTimer);
            window._sifencyInitTimer = setTimeout(() => {
                elements = [];                    // clear stale refs
                initScrollEffects();              // re-register all nodes
            }, 300);
        });
    });

})(jQuery);