// Pure altitude-scrub math — no DOM. A continuous `pos` (float) is the knob:
// integers are altitudes/detents, fractions are mid-morph blend. UMD so the
// browser gets a `HEFScrub` global and `node --test` can require() it.
(function (root, factory) {
  const api = factory();
  if (typeof module !== "undefined" && module.exports) module.exports = api;
  else root.HEFScrub = api;
})(typeof self !== "undefined" ? self : this, function () {
  "use strict";

  const clamp01 = (x) => Math.max(0, Math.min(1, x));
  const wrapIndex = (i, n) => ((i % n) + n) % n;

  function segmentOf(pos) {
    const lo = Math.floor(pos);
    return { lo, frac: pos - lo };
  }

  function accumToPos(restPos, accumDeg, stepDeg) {
    return restPos + accumDeg / stepDeg;
  }

  function fracToTime(frac, duration) {
    return clamp01(frac) * (duration || 0);
  }

  function crossfadeGains(frac) {
    const f = clamp01(frac);
    return { from: 1 - f, to: f };
  }

  // Integers strictly crossed moving prevPos -> pos, in travel order. Landing
  // exactly on an integer counts as crossing it (it commits that altitude).
  function integerCrossings(prevPos, pos) {
    const out = [];
    if (pos > prevPos) {
      for (let k = Math.floor(prevPos) + 1; k <= pos + 1e-9; k++) out.push({ index: k, dir: 1 });
    } else if (pos < prevPos) {
      for (let k = Math.ceil(prevPos) - 1; k >= pos - 1e-9; k--) out.push({ index: k, dir: -1 });
    }
    return out;
  }

  return { clamp01, wrapIndex, segmentOf, accumToPos, fracToTime, crossfadeGains, integerCrossings };
});
