/* ============================================================
   Arc Card Carousel — replica of the Framer Marketplace
   "Arc Card Carousel". Vanilla JS, no dependencies.

     new ArcCards(el, { cards: [{ art, title, num }, ...] })

   Measured law. The cards ride the top of a wheel whose centre
   sits far below the frame, and each one is rotated to stay
   tangent to it — so the row bends away at both edges and the
   cards lean as they go.

     radius   1576px          (centre 1576 below the top card)
     step     15deg per card
     cycle    120deg          (8 cards; angles wrap into [-60,60))
     card     325 x 460, unscaled at every angle
     opacity  clamp((60 - |angle|) / 20, 0, 1)
     drag     0.0718 deg per px, no snap, no inertia

   The opacity ramp is the part that is easy to get wrong. It is
   not a per-slot table: it is a continuous function of the angle,
   flat at 1 until 40deg and reaching 0 exactly at 60deg, which is
   the wrap point. That is why a card never blinks — it is already
   fully transparent at the instant it is recycled.

   Every card is the SAME SIZE at every angle. There is no scale
   ramp here at all; the growing bounding boxes toward the edges
   are just a rotated rectangle's box, not a bigger card.
   ============================================================ */

class ArcCards {
  static defaults = {
    w: 325,            // measured
    h: 460,            // measured (image 426 + label row)
    radius: 1576,      // measured
    step: 15,          // deg, measured
    topY: 295,         // px from the frame top to the centre of the top card
    fadeFrom: 40,      // deg — opacity is 1 up to here
    fadeTo: 60,        // deg — and 0 here, which is also the wrap point
    perDeg: 0.0718,    // deg of wheel per px of drag, measured
    ease: 'cubic-bezier(.22,.61,.36,1)',
    dur: 520,          // chosen — the reference does not snap, so nothing
                       // ever animates on its own; this is only for to()
  };

  constructor(root, options = {}) {
    this.root = typeof root === 'string' ? document.querySelector(root) : root;
    this.o = { ...ArcCards.defaults, ...options };
    this.cards = options.cards || [];
    this.root.classList.add('acc');
    this.angle = 0;            // wheel rotation, degrees, unbounded

    this.root.style.setProperty('--acc-w', this.o.w + 'px');
    this.root.style.setProperty('--acc-h', this.o.h + 'px');
    this.root.style.setProperty('--acc-r', this.o.radius + 'px');
    this.root.style.setProperty('--acc-top', this.o.topY + 'px');
    this.root.style.setProperty('--acc-dur', this.o.dur + 'ms');
    this.root.style.setProperty('--acc-ease', this.o.ease);

    this.render();
    this.bind();
    this.layout();
  }

  destroy() { removeEventListener('pointerup', this._up); removeEventListener('pointermove', this._move); }

  render() {
    const media = (m) => (typeof m === 'string' && /^(https?:|\.|\/|data:)/.test(m)
      ? '<img src="' + m + '" alt="">'
      : '<div class="acc__art">' + (m ?? '') + '</div>');
    this.root.innerHTML =
      '<div class="acc__wheel">' +
        this.cards.map((c, i) => '<article class="acc__card" data-i="' + i + '">' +
          '<div class="acc__shot">' + media(c.art) + '</div>' +
          '<div class="acc__meta"><span class="acc__title">' + (c.title || '') + '</span>' +
          '<span class="acc__num">' + (c.num || String(i + 1).padStart(3, '0')) + '</span></div>' +
        '</article>').join('') +
      '</div>';
    this.els = [...this.root.querySelectorAll('.acc__card')];
  }

  bind() {
    let on = false, sx = 0, start = 0, moved = false;
    this.root.addEventListener('pointerdown', (e) => {
      on = true; sx = e.clientX; start = this.angle; moved = false;
      this.root.classList.add('is-dragging');
      this.root.setPointerCapture?.(e.pointerId);
    });
    this._move = (e) => {
      if (!on) return;
      const dx = e.clientX - sx;
      if (!moved && Math.abs(dx) < 3) return;
      moved = true;
      /* The reference follows the pointer exactly and stops where it is let
         go — no snap to a card, no throw. Anything else here would be ours. */
      this.angle = start + dx * this.o.perDeg;
      this.layout();
      e.preventDefault();
    };
    this._up = () => {
      if (!on) return;
      on = false;
      this.root.classList.remove('is-dragging');
    };
    this.root.addEventListener('pointermove', this._move);
    addEventListener('pointerup', this._up);
    this.root.addEventListener('pointercancel', this._up);

    addEventListener('keydown', (e) => {
      if (e.key === 'ArrowLeft') this.to(this.angle - this.o.step);
      if (e.key === 'ArrowRight') this.to(this.angle + this.o.step);
    });
  }

  /* Animated move, for the keyboard only — the drag path writes `angle`
     directly so the cards never lag behind the hand. */
  to(a) {
    this.root.classList.add('is-easing');
    this.angle = a;
    this.layout();
    clearTimeout(this._t);
    this._t = setTimeout(() => this.root.classList.remove('is-easing'), this.o.dur);
  }

  layout() {
    const o = this.o;
    const cycle = o.step * this.els.length;      // 120deg for the measured 8
    const half = cycle / 2;
    this.els.forEach((el, i) => {
      /* Wrap into [-half, half) so a card leaving one end reappears at the
         other already invisible — the fade reaches 0 at exactly this bound. */
      let a = (i * o.step + this.angle) % cycle;
      if (a >= half) a -= cycle;
      if (a < -half) a += cycle;

      const fade = Math.max(0, Math.min(1, (o.fadeTo - Math.abs(a)) / (o.fadeTo - o.fadeFrom)));
      /* The card's box is centred on the wheel centre by negative margins, so
         its own transform-origin IS the wheel centre: translate up the radius
         first, then rotate about it. Doing the centring inside the transform
         instead would rotate about the wrong point. */
      el.style.transform =
        'rotate(' + a.toFixed(3) + 'deg) translateY(' + (-o.radius) + 'px)';
      el.style.opacity = fade.toFixed(3);
      el.style.zIndex = String(100 - Math.round(Math.abs(a)));
      el.style.pointerEvents = fade < 0.05 ? 'none' : '';
    });
  }
}

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