Effortless smooth values and ranges sliders
const slide = new Slider("#slider", {
	val: 50, // initial value
	min: 0, // min value
	max: 100, // max value
	col: colors(1) // color
});const slide = new Slider("#slider", {
	val: 50, 
	max: 100, 
	stp: 10, // step size
	col: colors(1)
});const slide = new Slider("#slider", {
	max: 100, 
	val: [8, 16, 32, 64], // 4 values
	col: colors(4), // 4 colors
});// two values + one color = range
const slide = new Slider("#slider", {
	val: [25, 75], // initial range
	col: colors(1) // single color
});// minimum range = 10
const slide = new Slider("#slider", {
	val: [25, 75],
	rng: 10, // min range
	col: colors(1)
});// n values + n / 2 colors = ranges
const slide = new Slider("#slider", {
	val: [5, 10, 20, 35, 50, 80], // 6 values
	col: colors(3), // 3 colors
});// n values + (n - 1) colors = linked ranges
const slide = new Slider("#slider", {
	val: [10, 50, 70], // 3 values
	col: colors(2) // 2 colors
});// 3x linked ranges
const slide = new Slider("#slider", {
	val: [10, 20, 50, 70], // 4 values
	col: colors(3) // 3 colors
});// time format function
const slide = new Slider("#slider", {
	max: 5400, // 1 hour 30 minutes
	val: 3600, // 1 hour
	col: colors(1), 
	fmt: s => // format function
		(s < 3600 ? [60, 1] : [3600, 60, 1])
		.map(x => `0${~~(s / x) % 60}`.slice(-2))
		.join(":")
});// date format function
const slide = new Slider("#slider", {
	min: new Date( // January 1st
		new Date().getFullYear(), 0, 1
	).getTime(),
	max: new Date( // December 31
		new Date().getFullYear(), 11, 31
	).getTime(),
	stp: 86400000, // 1 day step
	rng: 86400000, // 1 day min range
	col: colors(), // range color
	val: [ // initial range = [January 1st, now]
		new Date(new Date().getFullYear(), 0, 1), 
		Date.now()
	],
	fmt: s => // format function
		new Date(s).toLocaleDateString()
});const slide = new Slider("#slider", {
	min: 0,
	max: 0.1,
	stp: 0.001,
	val: [0.016, 0.032, 0.064],
	col: colors(3)
});const slide = new Slider("#slider", {
	min: 65,
	max: 90,
	rng: 1,
	col: colors(),
	val: [65, 70],
	fmt: s =>
		String.fromCharCode(s)
});<div id="vals"></div>
<div id="range"></div>
<div id="links"></div>
<div class="slides"></div>body {
	display: flex;
	justify-content: space-evenly;
	align-items: center;
}
.slider {
	line-height: 1;
}
.slider.vert {
    height: 240px;
}
.slides {
	display: none;
}const vals = new Slider("#vals", {
	max: 64,
	val: [8, 16, 32],
	col: colors(3),
	dir: 1
});
const range = new Slider("#range", {
	max: 64,
	val: [16, 32],
	col: colors(),
	dir: 1
});
const links = new Slider("#links", {
	min: 1,
	max: 9,
	stp: 1,
	val: [2, 4, 6],
	col: colors(2),
	dir: 1
});const colors = (num = 1) => [
	"ff595e", "ff924c", "ffca3a", 
	"c5ca30", "8ac926", "52a675", 
	"1982c4", "4267ac", "6a4c93"]
.sort(() => Math.random() - 0.5)
.slice(0, num)
.map(col => "#" + col);:root {
	--slideSize: 36px; /* slider size */
	--trackSize: 6px; /* slide track size */
	--focusSize: 10px; /* sliding track size */
	--trackColor: #e5e7eb; /* track background color */
	--textColor: #6b7280; /* min & max labels color */
	--fontSize: 14px; /* labels font size */
	--animate: 0.2s ease; /* sliding state animation */
}<div id="slider"></div>
<div class="slides"></div>body {
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
}
.slider {
	line-height: 1;
	width: 360px;
	max-width: 360px;
	max-width: 90%;
}
.slider.vert {
    height: 240px;
}
.slides {
	font-size: var(--fontSize);
	user-select: none;
	-webkit-user-select: none;
	pointer-events: none;
}const slides = document
.querySelector(".slides");
const disp = dat => 
	slides.innerHTML = [dat]
	.flat()
	.map(
		(val, idx) =>
			`<font color="${slide.colors[idx]}">${JSON.stringify(val)}</font>`
	)
	.join("  ");
slide
.on(
	"slide", 
	evt => 
		disp(evt.detail)
);
slide
.on(
	"change", 
	evt => 
		disp(evt.detail)
);
disp(slide.values);/**
 * @force
 * @export
 * @class Slider :
 */
class Slider {
	/**
	 * @construct
	 * @param {HTMLElement|string} container :
	 * @param {!SliderOptions=} opts :
	 */
	constructor(container, opts = {}) {
		opts = {
			min: 0,
			max: 100,
			stp: 1,
			num: 1,
			col: "#4A90E2",
			fmt: v =>
				v.toFixed(this.dec),
			val: null,
			rng: 0,
			dir: 0,
			...opts
		};
		this.track
			= this.ranges
			= this.valueBox
			= this.labelBox
			= this.minLabel
			= this.maxLabel
			= this.valueLabels
			= this._frameLoop
				= null;
		this.dec = String(opts.stp)
		.includes(".") ? String(opts.stp)
			.split(".")[1].length : 0;
		this.wrap = typeof container === "string" ? document.querySelector(container) : container;
		this._dir = opts.dir;
		this._step = opts.stp;
		this._range = opts.max - opts.min;
		this._disp = opts.fmt;
		this._colors = [opts.col].flat();
		this._ranged = opts.rng;
		this._min = opts.min;
		this._max = opts.max;
		
		this._activePointers = new Map();
		this._isMoving = false;
		this._frameLoop = null;
		const values = opts.val !== null ? [opts.val].flat() : this._defaultValues(opts.num);
		this._num = values.length;
		this._vals = Array(this._num)
		.fill(0);
		this._targetVals = Array(this._num)
		.fill(0);
		
		this._evts = new NativeEventTarget();
		this._createElements();
		this._setupEvents();
		this.values = values;
	
	}
	_createElements() {
		const c = (cls = "") =>
			Object.assign(
				document.createElement("div"),
				{
					className: cls
				}
			);
		
		this.wrap.classList.add("slider");
		if(this._dir)
			this.wrap.classList.add("vert");
		// this.wrap.tabIndex = 0;
		this.track = c("track");
		this.valueBox = c("vals");
		this.labelBox = c("lbls");
		this.minLabel = c();
		this.maxLabel = c();
		this.ranges = Array(Math.max(
			this._num,
			this._colors.length
		))
		.fill(0)
		.map(() =>
			c("range"));
		this.valueLabels = Array(this._num)
		.fill(0)
		.map(() =>
			c("val"));
		this.valueBox.append(...this.valueLabels);
		this.track.append(...this.ranges);
		this.labelBox.append(
			this.minLabel,
			this.maxLabel
		);
		this.wrap.append(
			this.valueBox,
			this.track,
			this.labelBox
		);
		this.minLabel.innerText = this._disp(this._min);
		this.maxLabel.innerText = this._disp(this._max);
	
	}
	_setupEvents() {
		this.wrap.addEventListener(
			"pointerdown",
			e =>
				this._onDown(e)
		);
		this.wrap.addEventListener(
			"pointermove",
			e =>
				this._onMove(e)
		);
		this.wrap.addEventListener(
			"pointerup",
			e =>
				this._onUp(e)
		);
		this.wrap.addEventListener(
			"pointercancel",
			e =>
				this._onUp(e)
		);
	
	}
	_defaultValues(n) {
		const stp = this._range / (n + 1);
		return Array(n)
		.fill(0)
		.map((_, i) =>
			this._roundToStep(this._min + stp * (i + 1)));
	
	}
	/**
	 * @export
	 * @getter
	 * @type {number|Array} values : slider values
	 */
	get values() {
		const vals = this._vals.map(v =>
			this._roundToStep(v));
		if(this._num === 1)
			return vals[0];
		if(this._colors.length >= this._num)
			return vals;
		if(this._colors.length === this._num - 1) {
			const sorted = [...vals].sort((a, b) =>
				a - b);
			return Array(sorted.length - 1)
			.fill(0)
			.map((_, i) =>
				[sorted[i], sorted[i + 1]]);
		
		}
		if(this._num % 2 === 0) {
			const ranges = [];
			for(let i = 0; i < vals.length; i += 2)
				ranges.push([vals[i], vals[i + 1]].sort((a, b) =>
					a - b));
			return ranges.length === 1 ? ranges[0] : ranges;
		
		}
		const sorted = [...vals].sort((a, b) =>
			a - b);
		return Array(sorted.length - 1)
		.fill(0)
		.map((_, i) =>
			[sorted[i], sorted[i + 1]]);
	
	}
	/**
	 * @setter
	 * @type {number|Array} values : slider values
	 */
	set values(v) {
		const newVals = [v].flat();
		if(newVals.length !== this._num)
			throw new Error(`${this._num} values expected`);
		this._vals = newVals.map(v =>
			this._forceRange(+v));
		this._targetVals = [...this._vals];
		this._updateUI();
	
	}
	/**
	 * @export
	 * @getter
	 * @type {Array} colors : slider colors
	 */
	get colors() {
		return this._colors;
	}
	/**
	 * @setter
	 * @type {Array} colors : slider colors
	 */
	set colors(c) {
		this._colors = [c].flat();
		this._updateUI();
	}
	_calculateLabelPositions(pos) {
		const rSize = this._dim(this.wrap);
		if(rSize === 0)
			return pos;
		const minGap = 4;
		// 1. Create label data objects and sort by initial position
		let labels = pos.map((p, i) => {
			const labelEl = this.valueLabels[i];
			const fullSize = this._dim(labelEl);
			return {
				idx: i,
				px: (p / 100) * rSize,
				fullSize: fullSize,
				halfSize: fullSize / 2
			};
		
		})
		.sort((a, b) =>
			a.px - b.px);
		if(labels.length === 0)
			return pos;
		// 2. Iteratively resolve overlaps by simulating repulsive forces
		for(let iter = 0; iter < 100; iter++) {
			let forces = Array(labels.length)
			.fill(0);
			let systemStable = true;
			// Calculate repulsive forces between overlapping labels
			for(let i = 0; i < labels.length - 1; i++) {
				const l1 = labels[i];
				const l2 = labels[i + 1];
				const requiredDist = l1.halfSize + l2.halfSize + minGap;
				const actualDist = l2.px - l1.px;
				if(actualDist < requiredDist) {
					systemStable = false;
					const overlap = requiredDist - actualDist;
					forces[i] -= overlap / 2;
					forces[i + 1] += overlap / 2;
				
				}
			
			}
			// Calculate boundary forces to keep labels inside the track
			const first = labels[0];
			if(first.px < first.halfSize) {
				systemStable = false;
				forces[0] += (first.halfSize - first.px);
			
			}
			const last = labels[labels.length - 1];
			if(last.px > rSize - last.halfSize) {
				systemStable = false;
				forces[labels.length - 1] -= (last.px - (rSize - last.halfSize));
			
			}
			if(systemStable)
				break;
			// Apply forces to update label positions
			labels.forEach((label, i) => {
				label.px += forces[i];
			
			});
		
		}
		// 3. Create the final result array with percentage values
		const result = Array(pos.length);
		labels.forEach(l => {
			result[l.idx] = (l.px / rSize) * 100;
		
		});
		return result;
	
	}
	_updateUI() {
		const pos = this._vals.map(v =>
			((v - this._min) / this._range) * 100);
		const cLen = this._colors.length;
		if(this._num % 2 === 0 && cLen < this._num && cLen !== this._num - 1) { // ranges
			const activeRangeIndices = new Set(Array.from(
				this._activePointers.values(),
				p =>
					Math.floor(p.activeIdx / 2)
			));
			const rangeData = [];
			for(let i = 0; i < this._num; i += 2) {
				const oidx = i / 2;
				rangeData.push({
					start: Math.min(
						pos[i],
						pos[i + 1]
					),
					end: Math.max(
						pos[i],
						pos[i + 1]
					),
					width: Math.abs(pos[i] - pos[i + 1]),
					color: this._colors[oidx % cLen],
					oidx,
					actv: activeRangeIndices.has(oidx)
				});
			
			}
			const zOrder = [...rangeData].sort((a, b) => {
				if(a.actv !== b.actv)
					return a.actv ? 1 : -1;
				return b.width - a.width;
			
			})
			.map(d =>
				d.oidx);
			rangeData.forEach(def => {
				const zIndex = zOrder.indexOf(def.oidx) + 1;
				this._updateLine(
					this.ranges[def.oidx],
					def.start,
					def.width,
					def.color,
					zIndex
				);
			
			});
		
		}
		else { // single | multiple values
			const sortedForZ = this._vals.map((v, i) =>
				({
					v, i
				}))
			.sort((a, b) =>
				b.v - a.v)
			.map(x =>
				x.i);
			if(cLen >= this._num) {
				pos.forEach((p, i) =>
					this._updateLine(
						this.ranges[i],
						0,
						p,
						this._colors[i],
						sortedForZ.indexOf(i) + 1
					));
			
			}
			else { // linked ranges
				const sVals = [...this._vals].sort((a, b) =>
					a - b);
				for(let i = 0; i < this._num - 1; i++) {
					const p1 = ((sVals[i] - this._min) / this._range) * 100;
					const p2 = ((sVals[i + 1] - this._min) / this._range) * 100;
					this._updateLine(
						this.ranges[i],
						p1,
						p2 - p1,
						this._colors[i % cLen],
						i + 1
					);
				
				}
			
			}
		
		}
		const adjPos = this._calculateLabelPositions(pos);
		this._vals.forEach((val, i) => {
			let cIdx;
			if(cLen >= this._num)
				cIdx = i;
			else if(this._num % 2 === 0 && cLen !== this._num - 1)
				cIdx = Math.floor(i / 2) % cLen;
			else {
				const sortedVals = [...new Set([...this._vals].sort((a, b) =>
					a - b))];
				cIdx = Math.min(
					sortedVals.indexOf(val),
					cLen - 1
				);
			
			}
			const label = this.valueLabels[i];
			label.innerText = this._disp(this._roundToStep(val));
			this._updateLabel(
				label,
				adjPos[i],
				this._colors[cIdx]
			);
		
		});
	
	}
	_updateLine(el, start, size, col, zdx) {
		Object.assign(
			el.style,
			{
				backgroundColor: col,
				zIndex: zdx
			}
		);
		if(this._dir) { // vert
			el.style.top = (100 - start - size) + "%";
			el.style.height = size + "%";
		
		}
		else {
			el.style.left = start + "%";
			el.style.width = size + "%";
		
		}
	
	}
	_updateLabel(el, lft, col) {
		const rSize = this._dim(this.wrap);
		const lSize = this._dim(el);
		const pxPos = lft / 100 * rSize;
		const edge = lSize / 2;
		let t = -50;
		if(pxPos < edge)
			t = -(pxPos / edge * 50);
		else if(pxPos > rSize - edge)
			t = -100 + (rSize - pxPos) / edge * 50;
		
		el.style.color = col;
		/*Object.assign(
			el.style,
			{
				color: col
			}
		);*/
		if(this._dir) { // vert
			el.style.top = (100 - lft) + "%";
			el.style.transform = `translateY(${t}%)`;
		
		}
		else {
			el.style.left = lft + "%";
			el.style.transform = `translateX(${t}%)`;
		
		}
	
	}
	_roundToStep(v) {
		return this._forceRange(+parseFloat(this._step * Math.round(v / this._step))
		.toFixed(this.dec));
	}
	_forceRange(v) {
		return Math.max(
			this._min,
			Math.min(
				this._max,
				v
			)
		);
	}
	_onDown(e) {
		if(!!e.button)
			return;
		
		const pos = this._pos(e);
		const val = this._min + pos * this._range;
		const usedIndices = Array.from(
			this._activePointers.values(),
			p =>
				p.activeIdx
		);
		let closestIdx = -1, minDiff = Infinity;
		this._vals.forEach((v, i) => {
			if(usedIndices.includes(i))
				return;
			const diff = Math.abs(val - v);
			if(diff < minDiff) {
				minDiff = diff;
				closestIdx = i;
			}
		
		});
		if(closestIdx !== -1) {
			const pid = e.pointerId;
			this.wrap.setPointerCapture(pid);
			this._activePointers.set(
				pid,
				{
					activeIdx: closestIdx,
					startVal: this._vals[closestIdx],
					startPos: pos
				}
			);
			this._slideState(true);
			this._updateUI(); // update zindex on down
		
		}
	
	}
	_onMove(e) {
		const pointer = this._activePointers.get(e.pointerId);
		if(!pointer)
			return;
		const pos = this._pos(e);
		const delta = pos - pointer.startPos;
		let newVal = this._roundToStep(pointer.startVal + delta * this._range);
		if(this._ranged && this._num % 2 === 0 && this._colors.length < this._num) {
			const pIdx = pointer.activeIdx % 2 === 0 ? pointer.activeIdx + 1 : pointer.activeIdx - 1;
			const pVal = this._vals[pIdx];
			const isLower = pointer.activeIdx % 2 === 0;
			if(isLower ? newVal > pVal : newVal < pVal) {
				this._targetVals[pIdx] = newVal;
				this._targetVals[pointer.activeIdx] = pVal;
				[pointer.activeIdx, pointer.startVal, pointer.startPos] = [pIdx, pVal, pos];
				return this._animate();
			
			}
			newVal = isLower ? Math.min(
				newVal,
				pVal - this._ranged
			) : Math.max(
				newVal,
				pVal + this._ranged
			);
		
		}
		if(!this._isMoving) {
			this._isMoving = true;
			this._dispatch("start");
		
		}
		this._targetVals[pointer.activeIdx] = newVal;
		this._animate();
	
	}
	_onUp(e) {
		const pid = e.pointerId;
		if(!this._activePointers.has(pid))
			return;
		this.wrap.releasePointerCapture(pid);
		this._activePointers.delete(pid);
		if(this._activePointers.size === 0) {
			this._isMoving = false;
			this._slideState(false);
			this._updateUI(); // reset zindex
		
		}
	
	}
	_pos(evt) {
		const rect = this.wrap.getBoundingClientRect();
		return this._dir ? 1 - (evt.clientY - rect.top) / rect.height : (evt.clientX - rect.left) / rect.width;
	
	}
	_dim(elt) {
		return this._dir ? elt.offsetHeight : elt.offsetWidth;
	
	}
	_animate() {
		if(this._frameLoop)
			return;
		const step = () => {
			let needsUpdate = false,
				valueChanged = false;
			const EASE = 0.2,
				PRECISION = this._step / 10;
			this._vals.forEach((val, i) => {
				const delta = this._targetVals[i] - val;
				if(Math.abs(delta) > 0) {
					if(Math.abs(delta) < PRECISION) {
						this._vals[i] = this._targetVals[i];
						valueChanged = true;
					}
					else {
						this._vals[i] += delta * EASE;
						needsUpdate = true;
					}
				
				}
			
			});
			if(needsUpdate || valueChanged) {
				this._updateUI();
				this._dispatch("slide");
				if(needsUpdate)
					this._frameLoop = requestAnimationFrame(step);
				else {
					this._frameLoop = null;
					this._dispatch("change");
				}
			
			}
			else
				this._frameLoop = null;
		
		};
		this._frameLoop = requestAnimationFrame(step);
	
	}
	_slideState(stt) {
		this.wrap.classList.toggle(
			"sliding",
			stt
		);
	}
	/**
	 * @force
	 * @export
	 * @method on :
	 * @param {string} evt :
	 * @param {Function} handler :
	 */
	on(evt, handler) {
		this._evts.addEventListener(
			evt,
			handler
		);
		return this;
	}
	/**
	 * @force
	 * @export
	 * @method off :
	 * @param {string} evt :
	 * @param {Function} handler :
	 */
	off(evt, handler) {
		this._evts.removeEventListener(
			evt,
			handler
		);
	}
	_dispatch(type) {
		this._evts.dispatchEvent(new CustomEvent(
			type,
			{
				detail: this.values
			}
		));
	}
	
	destroy() {
		if(this._frameLoop)
			cancelAnimationFrame(this._frameLoop);
		this.wrap.remove();
	
	}
}
// closure compiler warning
var NativeEventTarget = self["EventTarget"];:root {
	--slideSize: 36px;
	--trackSize: 6px;
	--focusSize: 10px;
	--trackColor: #e5e7eb;
	--textColor: #6b7280;
	--fontSize: 14px;
	--animate: 0.2s ease;
	--textSpc: calc(var(--trackSize) * 1);
	--textOff: calc((var(--fontSize) + var(--textSpc)) * -1);
	--slidSpc: calc(var(--focusSize) * 3 / 4);
	--slidOff: calc((var(--fontSize) + var(--slidSpc)) * -1);
}
@media (prefers-color-scheme: dark) {
	:root {
		--trackColor: #363636;
		--textColor: #9ca3af;
	}
}
.slider {
	position: relative;
	height: var(--slideSize);
	color: var(--textColor);
	cursor: pointer;
	-webkit-tap-highlight-color: transparent;
	touch-action: none;
	user-select: none;
	outline: none;
}
.slider.sliding {
	/* cursor: grabbing; */
}
.slider div {
	user-select: none;
	-webkit-user-select: none;
}
.slider .track {
	position: absolute;
	top: 50%;
	left: 0;
	width: 100%;
	height: var(--trackSize);
	transform: translateY(-50%);
	background: var(--trackColor);
	will-change: height;
	transition: height var(--animate);
}
.slider .range {
	position: absolute;
	height: 100%;
	will-change: left, width;
}
.slider .vals,
.slider .lbls {
	position: absolute;
	width: 100%;
	font-size: var(--fontSize);
	pointer-events: none;
	transition: transform var(--animate);
}
.slider .vals {
	top: 50%;
	transform: translateY(var(--textOff));
}
.slider .val {
	position: absolute;
	white-space: nowrap;
	will-change: left, transform;
}
.slider .lbls {
	top: 50%;
	display: flex;
	justify-content: space-between;
	transform: translateY(var(--textSpc));
}
.slider.sliding .track {
	height: var(--focusSize);
}
.slider.sliding .vals {
	transform: translateY(var(--slidOff));
}
.slider.sliding .lbls {
	transform: translateY(var(--slidSpc));
}
/* vertical */
.slider.vert {
	width: var(--slideSize);
}
.slider.vert .track {
	top: 0;
	left: 50%;
	width: var(--trackSize);
	height: 100%;
	transform: translateX(-50%);
	will-change: width;
	transition: width var(--animate);
}
.slider.vert .range {
	width: 100%;
	will-change: top, height;
}
.slider.vert .vals,
.slider.vert .lbls {
	top: 0;
	width: auto;
	height: 100%;
	/* line-height: .8rem; */
	line-height: var(--fontSize);
}
.slider.vert .vals {
	right: 50%;
	transform: translate(calc(var(--textSpc) * -1), 0);
}
.slider.vert .val {
	right: -100%;
	will-change: top, transform;
}
.slider.vert .lbls {
	left: 50%;
	flex-direction: column-reverse;
	transform: translateX(var(--textSpc));
}
.slider.vert.sliding .track {
	width: var(--focusSize);
}
.slider.vert.sliding .vals {
	transform: translateX(calc(var(--focusSize) * -1));
}
.slider.vert.sliding .lbls {
	transform: translateX(var(--focusSize));
}Just seeking ♪♬
Swipe or click
<div id="seektrack"></div>body {
	height: 100%;
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
}
.seektrack {
	width: 360px;
	max-width: 360px;
	max-width: 90%;
}const seekDatTrack = new SeekTrack(
	document.querySelector("#seektrack"), 
	v => console.log(v));
seekDatTrack.dur = 180;
seekDatTrack.val = 100;/**
 * @force
 * @export
 * @class SeekTrack : seek that track
 */
class SeekTrack {
	/**
	 * @construct
	 * @param {HTMLElement} wrap :
	 * @param {Function} seek :
	 */
	constructor(wrap, seek) {
		[this._track, this._timed, this._texts, this._start, this._total] 
			= ["track", "timed", "texts", "start", "total"].map(cls => {
				const el = document.createElement("div");
				el.classList.add(cls);
				return el;
			
			});
		// Init state
		this._min 
		= this._max 
		= this._val 
		= this._aim 
		= this._startPos 
		= this._startVal 
		
		= 0;
		this._down 
		= this._moved 
		= this._anm 
		= false;
		this._lst
		= this._frm 
		= this._rect 
		= null;
		this._cbk = seek;
		this._wrap = wrap;
		this._wrap.className = "seektrack";
		
		this._track.append(this._timed);
		this._texts.append(
			this._start,
			this._total
		);
		this._wrap.append(
			this._texts,
			this._track
		);
		this._wrap["onpointerdown"] = e => {
			this._rect = this._wrap.getBoundingClientRect();
			this._down = true;
			this._moved = false;
			this._startPos = e.clientX - this._rect.left;
			this._startVal = this._val;
			this._wrap.setPointerCapture(e.pointerId);
		
		};
		
		this._wrap["onpointermove"] = e => {
			if(!this._down) 
				return;
			this._moved = true;
			
			const pos = (e.clientX - this._rect.left) / this._rect.width;
			const delta = pos - this._startPos / this._rect.width;
			this._aim = Math.max(
				this._min,
				Math.min(
					this._max, 
					Math.round(this._startVal + delta * this._max)
				)
			);
			this._animate();
		
		};
		
		this._wrap["onpointerup"] = this._wrap["onpointercancel"] = e => {
			if(!this._down) 
				return;
			this._down = false;
			this._wrap.releasePointerCapture(e.pointerId);
			
			if(!this._moved) {
				const clickPos = (e.clientX - this._rect.left) / this._rect.width;
				this._aim = Math.max(
					this._min,
					Math.min(
						this._max, 
						Math.round(clickPos * this._max)
					)
				);
				this._animate();
			
			}
		
		};
		this._updateUI();
	
	}
	_fmt(s) {
		return (s < 3600 ? [60, 1] : [3600, 60, 1])
		.map(x => 
			`0${~~(s / x) % 60}`.slice(-2))
		.join(":");
	
	}
	_animate() {
		if(this._frm) 
			cancelAnimationFrame(this._frm);
		this._anm = true;
		const step = () => {
			const delta = this._aim - this._val;
			if(Math.abs(delta) < 0.2) {
				this._val = this._aim;
				this._updateUI();
				this._frm = null;
				this._anm = false;
				if(this.val !== this._lst) {
					this._lst = this.val;
					this._cbk(this.val);
				
				}
			
			}
			else {
				this._val += delta * 0.2;
				this._updateUI();
				this._frame(step);
			
			}
		
		};
		this._frame(step);
	
	}
	_frame(cb) {
		this._frm = requestAnimationFrame(cb);
	
	}
	_updateUI() {
		this._timed.style.width = (this._val / this._max) * 100 + "%";
		this._start.textContent = this._fmt(this._val);
		this._total.textContent = this._fmt(this._max - this._val);
	
	}
	/**
	 * @force
	 * @export
	 * @getter
	 * @type {number} val : current time
	 */
	get val() {
		return Math.round(this._val);
	
	}
	/**
	 * @setter
	 */
	set val(v) {
		if(this._down || this._anm) 
			return;
		this._val = this._aim = Math.max(
			this._min,
			Math.min(
				this._max,
				+v
			)
		);
		this._updateUI();
	
	}
	/**
	 * @force
	 * @export
	 * @getter
	 * @type {number} dur : total value
	 */
	get dur() {
		return this._max;
	
	}
	/**
	 * @setter
	 */
	set dur(v) {
		this._max = v;
		this._updateUI();
	
	}
	destroy() {
		if(this._frm) 
			cancelAnimationFrame(this._frm);
		this._wrap.remove();
	
	}
}.seektrack {
	position: relative;
	width: 100%;
	height: 100%;
	font-size: 13px;
	color: #6b7280;
	cursor: pointer;
	touch-action: none;
	user-select: none;
}
.seektrack .track {
	position: relative;
	width: 100%;
	height: 6px;
	background: #c8c8c8;
}
.seektrack .timed {
	width: 0%;
	height: 100%;
	will-change: width;
	background: #4A90E2;
}
.seektrack .texts {
	width: 100%;
	display: flex;
	justify-content: space-between;
	pointer-events: none;
	padding-bottom: .3rem;
}2K JS + 0.4K CSS
/**
 * Nico Pr — https://nicopr.fr
 * SEEKTRACK STANDALONE RELEASE v0.1.0
 * 27/04/2025 01:18:32
 */
(function(){
function c(a){a.o&&cancelAnimationFrame(a.o);a.s=!0;const e=()=>{const b=a.l-a.g;.2>Math.abs(b)?(a.g=a.l,d(a),a.o=null,a.s=!1,a.val!==a.H&&(a.H=a.val,a.J(a.val))):(a.g+=.2*b,d(a),a.o=requestAnimationFrame(e))};a.o=requestAnimationFrame(e)}function d(a){a.D.style.width=a.g/a.h*100+"%";a.I.textContent=f(a.g);a.F.textContent=f(a.h-a.g)}function f(a){return(3600>a?[60,1]:[3600,60,1]).map(e=>`0${~~(a/e)%60}`.slice(-2)).join(":")}
class g{constructor(a,e){[this.G,this.D,this.C,this.I,this.F]=["track","timed","texts","start","total"].map(b=>{const l=document.createElement("div");l.classList.add(b);return l});this.u=this.h=this.g=this.l=this.A=this.B=0;this.m=this.v=this.s=!1;this.H=this.o=this.j=null;this.J=e;this.i=a;this.i.className="seektrack";this.G.append(this.D);this.C.append(this.I,this.F);this.i.append(this.C,this.G);this.i.onpointerdown=b=>{this.j=this.i.getBoundingClientRect();this.m=!0;this.v=!1;this.A=b.clientX-
this.j.left;this.B=this.g;this.i.setPointerCapture(b.pointerId)};this.i.onpointermove=b=>{this.m&&(this.v=!0,this.l=Math.max(this.u,Math.min(this.h,Math.round(this.B+((b.clientX-this.j.left)/this.j.width-this.A/this.j.width)*this.h))),c(this))};this.i.onpointerup=this.i.onpointercancel=b=>{this.m&&(this.m=!1,this.i.releasePointerCapture(b.pointerId),this.v||(this.l=Math.max(this.u,Math.min(this.h,Math.round((b.clientX-this.j.left)/this.j.width*this.h))),c(this)))};d(this)}get val(){return Math.round(this.g)}set val(a){this.m||
this.s||(this.g=this.l=Math.max(this.u,Math.min(this.h,+a)),d(this))}get dur(){return this.h}set dur(a){this.h=a;d(this)}}var h=g,k=["SeekTrack"],m=this||self;k[0]in m||"undefined"==typeof m.execScript||m.execScript("var "+k[0]);for(var n;k.length&&(n=k.shift());)k.length||void 0===h?m[n]&&m[n]!==Object.prototype[n]?m=m[n]:m=m[n]={}:m[n]=h;}).call(this);.seektrack{position:relative;width:100%;height:100%;font-size:13px;color:#6b7280;cursor:pointer;touch-action:none;user-select:none}.seektrack .track{position:relative;width:100%;height:6px;background:#c8c8c8}.seektrack .timed{width:0%;height:100%;will-change:width;background:#4a90e2}.seektrack .texts{width:100%;display:flex;justify-content:space-between;pointer-events:none;padding-bottom:.3rem}