/* ============================================================
   Tilt Slider — replica of Framer Marketplace "3D Tilt Slider".
   Vanilla JS, no dependencies.

     new TiltSlider(el, { cards: [{ shot, role, name, desc }] })

   Measured behaviour: the cards do NOT tilt on hover. They rotate about
   Y with the DRAG, leaning into the direction of travel and capping at
   about 18deg, then relaxing back to 0 when you let go.

     step3  rotY -11.99
     step6  rotY -16.00
     step9  rotY -17.49
     step12 rotY -17.83     <- approaching the cap
     released, settling     -3.45
   ============================================================ */

class TiltSlider {
  static defaults = {
    maxTilt: 18,        // degrees, measured cap
    gain: 0.62,         // deg per px/frame of travel
    ease: 0.16,         // how fast the tilt chases its target
    friction: 0.92,
    loop: true,
    // Measured on the reference at rest: the strip drifts left at a steady
    // 40.0 px/s (scrollLeft, constant velocity - a linear marquee, not eased).
    // Set 0 to hold still.
    autoplay: 40,
    // The reference's idle drift moves the cards WITHOUT tilting them: an
    // ambient take showed x changing on every card and rotateY flat. So the
    // marquee is added to position directly and never feeds the tilt, which
    // stays driven by drag/wheel velocity alone.
  };

  constructor(root, options = {}) {
    this.root = typeof root === 'string' ? document.querySelector(root) : root;
    this.o = { ...TiltSlider.defaults, ...options };

    if (options.cards) this.#render(options.cards);
    this.track = this.root.querySelector('.tilt__track');
    this.cards = [...this.root.querySelectorAll('.tilt__card')];
    this.n = this.cards.length;

    this.x = 0; this.vx = 0; this.tilt = 0;

    this.drag = null;
    this.root.addEventListener('pointerdown', (e) => {
      this.drag = { sx: e.clientX, x0: this.x, t: performance.now(), lx: e.clientX };
      this.vx = 0; this.root.setPointerCapture(e.pointerId);
    });
    this.root.addEventListener('pointermove', (e) => {
      if (!this.drag) return;
      this.x = this.drag.x0 + (e.clientX - this.drag.sx);
      const dt = Math.max(1, performance.now() - this.drag.t);
      this.vx = (e.clientX - this.drag.lx) / dt * 16;
      this.drag.lx = e.clientX; this.drag.t = performance.now();
    });
    const end = () => { this.drag = null; };
    this.root.addEventListener('pointerup', end);
    this.root.addEventListener('pointercancel', end);
    this.root.addEventListener('wheel', (e) => {
      e.preventDefault();
      this.vx -= (Math.abs(e.deltaY) > Math.abs(e.deltaX) ? e.deltaY : e.deltaX) * 0.4;
    }, { passive: false });

    this.raf = requestAnimationFrame(this.tick = () => this.#frame());
  }

  destroy() { cancelAnimationFrame(this.raf); }

  get pitch() {
    if (this.n < 2) return this.cards[0]?.getBoundingClientRect().width ?? 0;
    return this.cards[1].offsetLeft - this.cards[0].offsetLeft;
  }

  #frame() {
    const now = performance.now();
    const dt = this.last ? Math.min(now - this.last, 64) : 16.7;   // clamp tab-switch spikes
    this.last = now;

    if (!this.drag) {
      if (Math.abs(this.vx) > 0.05) { this.x += this.vx; this.vx *= this.o.friction; }
      else this.vx = 0;
      // Ambient marquee. Time-based, so it holds 40 px/s off 60Hz too.
      if (this.o.autoplay) this.x -= this.o.autoplay * dt / 1000;
    }

    const span = this.pitch * this.n;
    if (this.o.loop) {
      if (this.x > 0) this.x -= span;
      if (this.x < -span) this.x += span;
    }
    this.track.style.transform = `translateX(${this.x.toFixed(2)}px)`;

    // Tilt chases a target set by travel speed, capped, and decays to 0.
    const target = Math.max(-this.o.maxTilt, Math.min(this.o.maxTilt, -this.vx * this.o.gain));
    this.tilt += (target - this.tilt) * this.o.ease;
    if (Math.abs(this.tilt) < 0.01) this.tilt = 0;

    const t = this.tilt.toFixed(2);
    for (const c of this.cards) c.style.transform = `rotateY(${t}deg)`;

    this.raf = requestAnimationFrame(this.tick);
  }

  #render(cards) {
    const media = (m) => (typeof m === 'string' && /^(https?:|\.|\/|data:)/.test(m)
      ? `<img src="${m}" alt="">`
      : `<div class="tilt__art">${m ?? ''}</div>`);
    // duplicate once so the loop has something to wrap into
    const list = this.o.loop ? cards.concat(cards) : cards;
    this.root.innerHTML = `<div class="tilt__track">
      ${list.map((c) => `<article class="tilt__card">
        <div class="tilt__shot">${media(c.shot)}</div>
        <p class="tilt__role">${c.role ?? ''}</p>
        <h3 class="tilt__name">${c.name ?? ''}</h3>
        <p class="tilt__desc">${c.desc ?? ''}</p>
        <div class="tilt__more"><i></i><i></i><i></i></div>
      </article>`).join('')}
    </div>`;
  }
}

if (typeof module !== 'undefined') module.exports = TiltSlider;
