<script lang="ts" module>
	export type ChangeEvent = {
		activeHandle: number;
		startValue: number;
		previousValue: number;
		value: number;
		values: number[];
	};

	export type StartEvent = { activeHandle: number; value: number; values: number[] };

	export type StopEvent = {
		activeHandle: number;
		startValue: number;
		value: number;
		values: number[];
	};

	export interface RangeSliderProps {
		range?: boolean | 'min' | 'max';
		onchange?: (event: ChangeEvent) => void;
		onstart?: (event: StartEvent) => void;
		onstop?: (event: StopEvent) => void;
		pushy?: boolean;
		min?: number;
		max?: number;
		ariaLabels?: string[];
		precision?: number;
		springOptions?: { stiffness: number; damping: number };
		id?: string;
		prefix?: string;
		suffix?: string;
		pips?: boolean;
		pipstep?: number;
		all?: boolean | 'pip' | 'label';
		first?: boolean | 'pip' | 'label';
		last?: boolean | 'pip' | 'label';
		rest?: boolean | 'pip' | 'label';
		step?: number;
		value?: number;
		values?: number[];
		vertical?: boolean;
		float?: boolean;
		reversed?: boolean;
		hoverable?: boolean;
		disabled?: boolean;
		formatter?: (value: number, index: number, percent: number) => string;
		handleFormatter?: (value: number, index: number, percent: number) => string;
	}
</script>

<script lang="ts">
	import { spring } from 'svelte/motion';
	import RangePips from './range-pips.svelte';

	let {
		range = false,
		pushy = false,
		min = 0,
		max = 100,
		ariaLabels = [],
		precision = 2,
		springOptions = { stiffness: 0.15, damping: 0.4 },
		id = '',
		prefix = '',
		suffix = '',
		pips = false,
		pipstep,
		all,
		first,
		last,
		rest,
		step = 1,
		value = $bindable(0),
		values = $bindable([(max + min) / 2]),
		vertical = false,
		float = false,
		reversed = false,
		hoverable = true,
		disabled = false,
		onchange,
		onstart,
		onstop,
		formatter = (value: { toString: () => string }) => value.toString(),
		handleFormatter = formatter
	}: RangeSliderProps = $props();

	if (value) {
		values = [value];
	}

	let slider: Element | undefined = $state(undefined);
	let valueLength = $state(0);
	let focus = $state(false);
	let handleActivated = $state(false);
	let handlePressed = $state(false);
	let keyboardActive = $state(false);
	let activeHandle = $state(values.length - 1);

	let startValue: number | undefined = $state();

	let previousValue: number | undefined = $state();

	/**
	 * make sure the value is coerced to a float value
	 * @param {number} v the value to fix
	 * @return {number} a float version of the input
	 **/
	const fixFloat = (v: number): number => parseFloat((+v).toFixed(precision));

	$effect(() => {
		// check that "values" is an array, or set it as array to prevent any errors in springs, or range trimming
		if (!Array.isArray(values)) {
			values = [(max + min) / 2];
			console.error(
				"'values' prop should be an Array (https://github.com/simeydotme/svelte-range-slider-pips#slider-props)"
			);
		}

		// trim the range so it remains as a min/max (only 2 handles)
		// and also align the handles to the steps
		const trimmedAlignedValues = trimRange(values.map((v) => alignValueToStep(v)));
		if (
			!(values.length === trimmedAlignedValues.length) ||
			!values.every((element, index) => fixFloat(element) === trimmedAlignedValues[index])
		) {
			values = trimmedAlignedValues;
		}

		// check if the valueLength (length of values[]) has changed,
		// because if so we need to re-seed the spring function with the new values array.
		if (valueLength !== values.length) {
			// set the initial spring values when the slider initialises, or when values array length has changed
			springPositions = spring(
				values.map((v) => percentOf(v)),
				springOptions
			);
		} else {
			// update the value of the spring function for animated handles whenever the values has updated
			springPositions.set(values.map((v) => percentOf(v)));
		}
		// set the valueLength for the next check
		valueLength = values.length;

		if (values.length > 1 && !Array.isArray(ariaLabels)) {
			console.warn(
				`'ariaLabels' prop should be an Array (https://github.com/simeydotme/svelte-range-slider-pips#slider-props)`
			);
		}
	});

	/**
	 * take in a value, and then calculate that value's percentage
	 * of the overall range (min-max);
	 * @param {number} val the value we're getting percent for
	 * @return {number} the percentage value
	 **/
	const percentOf = (/** @type {number} */ val: number): number => {
		let percent = ((val - min) / (max - min)) * 100;

		if (isNaN(percent) || percent <= 0) {
			return 0;
		}

		if (percent >= 100) {
			return 100;
		}

		return fixFloat(percent);
	};

	/**
	 * clamp a value from the range so that it always
	 * falls within the min/max values
	 * @param {number} val the value to clamp
	 * @return {number} the value after it's been clamped
	 **/
	const clampValue = (/** @type {number} */ val: number): number => {
		// return the min/max if outside of that range
		return val <= min ? min : val >= max ? max : val;
	};

	/**
	 * align the value with the steps so that it
	 * always sits on the closest (above/below) step
	 * @param {number} val the value to align
	 * @return {number} the value after it's been aligned
	 **/
	const alignValueToStep = (/** @type {number} */ val: number): number => {
		// sanity check for performance
		if (val <= min) {
			return fixFloat(min);
		}

		if (val >= max) {
			return fixFloat(max);
		}

		val = fixFloat(val);

		// find the middle-point between steps and see if the value is closer to the next step, or previous step
		let remainder = (val - min) % step;
		let aligned = val - remainder;

		if (Math.abs(remainder) * 2 >= step) {
			aligned += remainder > 0 ? step : -step;
		}

		aligned = clampValue(aligned); // make sure the value is within acceptable limits

		// make sure the returned value is set to the precision desired
		// this is also because javascript often returns weird floats
		// when dealing with odd numbers and percentages
		return fixFloat(aligned);
	};

	/**
	 * the orientation of the handles/pips based on the
	 * input values of vertical and reversed
	 * @type {"top"|"bottom"|"left"|"right"} orientationStart
	 **/
	let orientationStart: 'top' | 'bottom' | 'left' | 'right' = $derived(
		vertical ? (reversed ? 'top' : 'bottom') : reversed ? 'right' : 'left'
	);
	let orientationEnd = $derived(
		vertical ? (reversed ? 'bottom' : 'top') : reversed ? 'left' : 'right'
	);

	/**
	 * helper function to get the index of an element in it's DOM container
	 * @param {Element|null} el dom object reference we want the index of
	 * @returns {number} the index of the input element
	 **/
	function index(el: Element | null): number {
		if (!el) {
			return -1;
		}

		let i = 0;
		while ((el = el.previousElementSibling)) {
			i++;
		}
		return i;
	}

	/**
	 * normalise a mouse or touch event to return the
	 * client (x/y) object for that event
	 * @param {MouseEvent|TouchEvent} e a mouse/touch event to normalise
	 * @returns {{ x: number, y: number }} normalised event client object (x,y)
	 **/
	function normalisedClient(e: MouseEvent | TouchEvent): { x: number; y: number } {
		if (e.type.includes('touch')) {
			const touchEvent = e as TouchEvent;
			const touch = touchEvent.touches[0] || touchEvent.changedTouches[0];
			return { x: touch.clientX, y: touch.clientY };
		} else {
			const mouseEvent = e as MouseEvent;
			return { x: mouseEvent.clientX, y: mouseEvent.clientY };
		}
	}

	/**
	 * check if an element is a handle on the slider
	 * @param {Element} el dom object reference we want to check
	 * @returns {boolean}
	 **/
	function targetIsHandle(el: Element): boolean {
		if (!slider) return false;
		const handles = [...slider.querySelectorAll('.handle')];
		const isHandle = handles.includes(el);
		const isChild = handles.some((handle) => handle.contains(el));
		return isHandle || isChild;
	}

	/**
	 * trim the values array based on whether the property
	 * for 'range' is 'min', 'max', or truthy. This is because we
	 * do not want more than one handle for a min/max range, and we do
	 * not want more than two handles for a true range.
	 * @param {number[]} values the input values for the rangeSlider
	 * @return {number[]} the range array for creating a rangeSlider
	 **/
	function trimRange(values: number[]): number[] {
		if (range === 'min' || range === 'max') {
			return values.slice(0, 1);
		}
		if (range) {
			return values.slice(0, 2);
		}

		return values;
	}

	/**
	 * helper to return the slider dimensions for finding
	 * the closest handle to user interaction
	 * @return {DOMRect} the range slider DOM client rect
	 **/
	function getSliderDimensions(): DOMRect | undefined {
		return slider?.getBoundingClientRect();
	}

	/**
	 * helper to return closest handle to user interaction
	 * @param {{ x: number, y: number }} clientPos the client{x,y} positions to check against
	 * @return {number} the index of the closest handle to clientPos
	 **/
	function getClosestHandle(clientPos: { x: number; y: number }): number {
		// first make sure we have the latest dimensions
		// of the slider, as it may have changed size
		const dims = getSliderDimensions();
		if (!dims) throw new Error('No Slider Dimensions yet.');
		// calculate the interaction position, percent and value
		let handlePos = 0;
		let handlePercent = 0;
		let handleVal = 0;
		if (vertical) {
			handlePos = clientPos.y - dims.top;
			handlePercent = (handlePos / dims.height) * 100;
			handlePercent = reversed ? handlePercent : 100 - handlePercent;
		} else {
			handlePos = clientPos.x - dims.left;
			handlePercent = (handlePos / dims.width) * 100;
			handlePercent = reversed ? 100 - handlePercent : handlePercent;
		}
		handleVal = ((max - min) / 100) * handlePercent + min;

		// if we have a range, and the handles are at the same
		// position, we want a simple check if the interaction
		// value is greater than return the second handle
		if (range === true && values[0] === values[1]) {
			if (handleVal > values[1]) {
				return 1;
			}

			return 0;

			// if there are multiple handles, and not a range, then
			// we sort the handles values, and return the first one closest
			// to the interaction value
		}

		return values.indexOf(
			[...values].sort((a, b) => Math.abs(handleVal - a) - Math.abs(handleVal - b))[0]
		);
	}

	/**
	 * take the interaction position on the slider, convert
	 * it to a value on the range, and then send that value
	 * through to the moveHandle() method to set the active
	 * handle's position
	 * @param {{ x: number, y: number }} clientPos the client{x,y} of the interaction
	 **/
	function handleInteract(clientPos: { x: number; y: number }) {
		// first make sure we have the latest dimensions
		// of the slider, as it may have changed size
		const dims = getSliderDimensions();
		if (!dims) throw new Error('No Slider Dimensions yet.');
		// calculate the interaction position, percent and value
		let handlePos = 0;
		let handlePercent = 0;
		let handleVal = 0;
		if (vertical) {
			handlePos = clientPos.y - dims.top;
			handlePercent = (handlePos / dims.height) * 100;
			handlePercent = reversed ? handlePercent : 100 - handlePercent;
		} else {
			handlePos = clientPos.x - dims.left;
			handlePercent = (handlePos / dims.width) * 100;
			handlePercent = reversed ? 100 - handlePercent : handlePercent;
		}
		handleVal = ((max - min) / 100) * handlePercent + min;
		// move handle to the value
		moveHandle(activeHandle, handleVal);
	}

	let lastSetValue = NaN;
	/**
	 * move a handle to a specific value, respecting the clamp/align rules
	 * @param {number} index the index of the handle we want to move
	 * @param {number} handleValue the value to move the handle to
	 * @return {number} the value that was moved to (after alignment/clamping)
	 **/
	function moveHandle(index: number | undefined, handleValue: number): number {
		// align & clamp the value so we're not doing extra
		// calculation on an out-of-range value down below
		handleValue = alignValueToStep(handleValue);
		// use the active handle if handle index is not provided
		if (typeof index === 'undefined') {
			index = activeHandle;
		}
		// if this is a range slider perform special checks
		if (range) {
			// restrict the handles of a range-slider from
			// going past one-another unless "pushy" is true
			if (index === 0 && handleValue > values[1]) {
				if (pushy) {
					values[1] = handleValue;
				} else {
					handleValue = values[1];
				}
			} else if (index === 1 && handleValue < values[0]) {
				if (pushy) {
					values[0] = handleValue;
				} else {
					handleValue = values[0];
				}
			}
		}

		// if the value has changed, update it
		if (values[index] !== handleValue) {
			values[index] = handleValue;
		}

		// fire the change event when the handle moves,
		// and store the previous value for the next time
		if (previousValue !== handleValue) {
			handleOnChange();
			previousValue = handleValue;
		}
		lastSetValue = handleValue;
		value = handleValue;
		return handleValue;
	}
	$effect(() => {
		if (value !== lastSetValue) moveHandle(undefined, value);
	});

	/**
	 * helper to find the beginning range value for use with css style
	 * @param {number[]} values the input values for the rangeSlider
	 * @return {number} the beginning of the range
	 **/
	function rangeStart(values: number[]): number {
		if (range === 'min') {
			return 0;
		}

		return values[0];
	}

	/**
	 * helper to find the ending range value for use with css style
	 * @param {array} values the input values for the rangeSlider
	 * @return {number} the end of the range
	 **/
	function rangeEnd(values: Array<any>): number {
		if (range === 'max') {
			return 0;
		}

		if (range === 'min') {
			return 100 - values[0];
		}

		return 100 - values[1];
	}

	/**
	 * helper to take a string of html and return only the text
	 * @param {string} possibleHtml the string that may contain html
	 * @return {string} the text from the input
	 */
	function pureText(possibleHtml: string): string {
		return `${possibleHtml}`.replace(/<[^>]*>/g, '');
	}

	/**
	 * when the user has unfocussed (blurred) from the
	 * slider, deactivate all handles
	 **/
	function sliderBlurHandle() {
		if (!keyboardActive) {
			return;
		}

		focus = false;
		handleActivated = false;
		handlePressed = false;
	}

	/**
	 * when the user focusses the handle of a slider
	 * set it to be active
	 * @param {Event} e the event from browser
	 **/
	function sliderFocusHandle(e: Event) {
		if (disabled) {
			return;
		}

		const target = e.target as HTMLElement;
		activeHandle = index(target);
		focus = true;
	}

	/**
	 * handle the keyboard accessible features by checking the
	 * input type, and modfier key then moving handle by appropriate amount
	 * @param {KeyboardEvent} e the event from browser
	 **/
	function sliderKeydown(e: KeyboardEvent) {
		if (disabled) {
			return;
		}

		const target = e.target as HTMLElement;
		const handle = index(target);
		let jump = e.ctrlKey || e.metaKey || e.shiftKey ? step * 10 : step;
		let prevent = false;

		if (e.key === 'PageDown' || e.key === 'PageUp') {
			jump *= 10;
		}

		if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
			moveHandle(handle, values[handle] + jump);
			prevent = true;
		}

		if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
			moveHandle(handle, values[handle] - jump);
			prevent = true;
		}

		if (e.key === 'Home') {
			moveHandle(handle, min);
			prevent = true;
		}

		if (e.key === 'End') {
			moveHandle(handle, max);
			prevent = true;
		}

		if (prevent) {
			e.preventDefault();
			e.stopPropagation();
		}
	}

	/**
	 * function to run when the user touches the slider element anywhere
	 * @param {MouseEvent|TouchEvent} e the event from browser
	 **/
	function sliderInteractStart(e: MouseEvent | TouchEvent) {
		if (disabled) {
			return;
		}

		const element = e.target as HTMLElement;
		const clientPos = normalisedClient(e);
		// set the closest handle as active
		focus = true;
		handleActivated = true;
		handlePressed = true;
		activeHandle = getClosestHandle(clientPos);

		// fire the start event
		startValue = previousValue = alignValueToStep(values[activeHandle]);
		handleOnStart();

		// for touch devices we want the handle to instantly
		// move to the position touched for more responsive feeling
		if (e.type === 'touchstart' && !element.matches('.pipVal')) {
			handleInteract(clientPos);
		}
	}

	/**
	 * function to run when the user stops touching
	 * down on the slider element anywhere
	 * @param {Event} e the event from browser
	 **/
	function sliderInteractEnd(e: Event) {
		// fire the stop event for touch devices
		if (e.type === 'touchend') {
			handleOnStop();
		}
		handlePressed = false;
	}

	/**
	 * unfocus the slider if the user clicked off of
	 * it, somewhere else on the screen
	 * @param {MouseEvent|TouchEvent} e the event from browser
	 **/
	function bodyInteractStart(e: MouseEvent | TouchEvent) {
		keyboardActive = false;
		const target = e.target as HTMLElement;
		if (slider && focus && e.target !== slider && !slider.contains(target)) {
			focus = false;
		}
	}

	/**
	 * send the clientX through to handle the interaction
	 * whenever the user moves across screen while active
	 * @param {MouseEvent|TouchEvent} e the event from browser
	 **/
	function bodyInteract(e: MouseEvent | TouchEvent) {
		if (!disabled) {
			if (handleActivated) {
				handleInteract(normalisedClient(e));
			}
		}
	}

	/**
	 * if user triggers mouseup on the body while
	 * a handle is active (without moving) then we
	 * trigger an interact event there
	 * @param {MouseEvent|TouchEvent} e the event from browser
	 **/
	function bodyMouseUp(e: MouseEvent | TouchEvent) {
		if (!disabled) {
			const el = e.target as HTMLElement;
			// this only works if a handle is active, which can
			// only happen if there was sliderInteractStart triggered
			// on the slider, already
			if (handleActivated) {
				if (slider && (el === slider || slider.contains(el))) {
					focus = true;
					// don't trigger interact if the target is a handle (no need) or
					// if the target is a label (we want to move to that value from rangePips)
					if (!targetIsHandle(el) && !el.matches('.pipVal')) {
						handleInteract(normalisedClient(e));
					}
				}
				// fire the stop event for mouse device
				// when the body is triggered with an active handle
				handleOnStop();
			}
		}
		handleActivated = false;
		handlePressed = false;
	}

	/**
	 * @param {KeyboardEvent} e
	 */
	function bodyKeyDown(e: KeyboardEvent) {
		if (disabled) {
			return;
		}

		const target = e.target as HTMLElement;

		if (slider && (e.target === slider || slider.contains(target))) {
			keyboardActive = true;
		}
	}

	function handleOnStop() {
		if (disabled || !onstop || typeof onstop !== 'function') {
			return;
		}

		onstop({
			activeHandle,
			startValue: startValue ?? 0,
			value: values[activeHandle],
			values: values.map((v) => alignValueToStep(v))
		});
	}

	function handleOnStart() {
		if (disabled || !onstart || typeof onstart !== 'function') {
			return;
		}

		onstart({
			activeHandle,
			value: startValue ?? 0,
			values: values.map((v) => alignValueToStep(v))
		});
	}

	function handleOnChange() {
		if (disabled || !onchange || typeof onchange !== 'function') {
			return;
		}

		onchange({
			activeHandle,
			startValue: startValue ?? 0,
			previousValue: typeof previousValue === 'undefined' ? (startValue ?? 0) : previousValue,
			value: values[activeHandle],
			values: values.map((v) => alignValueToStep(v))
		});
	}

	/** @type {import('svelte/motion').Spring<number[]>} */
	let springPositions: import('svelte/motion').Spring<number[]> = spring(
		values.map((v) => percentOf(v)),
		springOptions
	);
</script>

<div
	{id}
	bind:this={slider}
	role="none"
	class="_rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28"
	class:range
	class:disabled
	class:hoverable
	class:vertical
	class:reversed
	class:focus
	class:min={range === 'min'}
	class:max={range === 'max'}
	class:pips
	class:pip-labels={all === 'label' || first === 'label' || last === 'label' || rest === 'label'}
	onmousedown={sliderInteractStart}
	onmouseup={sliderInteractEnd}
>
	{#each values as value, index}
		<span
			role="slider"
			class="rangeHandle"
			class:active={focus && activeHandle === index}
			class:press={handlePressed && activeHandle === index}
			data-handle={index}
			onblur={sliderBlurHandle}
			onfocus={sliderFocusHandle}
			onkeydown={sliderKeydown}
			style="{orientationStart}: {$springPositions[index]}%; z-index: {activeHandle === index
				? 3
				: 2};"
			aria-label={ariaLabels[index]}
			aria-valuemin={range === true && index === 1 ? values[0] : min}
			aria-valuemax={range === true && index === 0 ? values[1] : max}
			aria-valuenow={value}
			aria-valuetext="{prefix}{pureText(handleFormatter(value, index, percentOf(value)))}{suffix}"
			aria-orientation={vertical ? 'vertical' : 'horizontal'}
			aria-disabled={disabled}
			tabindex={disabled ? -1 : 0}
		>
			<span class="rangeNub"></span>
			{#if float}
				<span class="rangeFloat">
					{prefix}{handleFormatter(value, index, percentOf(value))}{suffix}
				</span>
			{/if}
		</span>
	{/each}

	{#if range}
		<span
			class="rangeBar"
			style="{orientationStart}: {rangeStart($springPositions)}%; 
             {orientationEnd}: {rangeEnd($springPositions)}%;"
		></span>
	{/if}

	{#if pips}
		<RangePips
			{values}
			{min}
			{max}
			{step}
			{range}
			{vertical}
			{reversed}
			{orientationStart}
			{hoverable}
			{disabled}
			{all}
			{first}
			{last}
			{rest}
			{pipstep}
			{prefix}
			{suffix}
			{formatter}
			{focus}
			{percentOf}
			{moveHandle}
			{fixFloat}
			{normalisedClient}
		/>
	{/if}
</div>

<svelte:window
	onmousedown={bodyInteractStart}
	onmousemove={bodyInteract}
	onmouseup={bodyMouseUp}
	onkeydown={bodyKeyDown}
/>

<style>
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28) {
		--slider: var(--range-slider, #d7dada);
		--handle-inactive: var(--range-handle-inactive, #99a2a2);
		--handle: var(--range-handle, #838de7);
		--handle-focus: var(--range-handle-focus, #4a40d4);
		--handle-border: var(--range-handle-border, var(--handle));
		--range-inactive: var(--range-range-inactive, var(--handle-inactive));
		--range: var(--range-range, var(--handle-focus));
		--float-inactive: var(--range-float-inactive, var(--handle-inactive));
		--float: var(--range-float, var(--handle-focus));
		--float-text: var(--range-float-text, white);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28) {
		position: relative;
		border-radius: 100px;
		height: 0.5em;
		margin: 1em;
		transition: opacity 0.2s ease;
		user-select: none;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 *) {
		user-select: none;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.pips) {
		margin-bottom: 1.8em;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.pip-labels) {
		margin-bottom: 2.8em;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical) {
		display: inline-block;
		border-radius: 100px;
		width: 0.5em;
		min-height: 200px;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical.pips) {
		margin-right: 1.8em;
		margin-bottom: 1em;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical.pip-labels) {
		margin-right: 2.8em;
		margin-bottom: 1em;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle) {
		position: absolute;
		display: block;
		height: 1.4em;
		width: 1.4em;
		top: 0.25em;
		bottom: auto;
		transform: translateY(-50%) translateX(-50%);
		z-index: 2;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.reversed .rangeHandle) {
		transform: translateY(-50%) translateX(50%);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical .rangeHandle) {
		left: 0.25em;
		top: auto;
		transform: translateY(50%) translateX(-50%);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical.reversed .rangeHandle) {
		transform: translateY(-50%) translateX(-50%);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeNub),
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle:before) {
		position: absolute;
		left: 0;
		top: 0;
		display: block;
		border-radius: 10em;
		height: 100%;
		width: 100%;
		transition: box-shadow 0.2s ease;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle:before) {
		content: '';
		left: 1px;
		top: 1px;
		bottom: 1px;
		right: 1px;
		height: auto;
		width: auto;
		box-shadow: 0 0 0 0px var(--handle-border);
		opacity: 0;
	}
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable:not(.disabled)
			.rangeHandle:hover:before
	) {
		box-shadow: 0 0 0 8px var(--handle-border);
		opacity: 0.2;
	}
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable:not(.disabled)
			.rangeHandle.press:before
	),
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable:not(.disabled)
			.rangeHandle.press:hover:before
	) {
		box-shadow: 0 0 0 12px var(--handle-border);
		opacity: 0.4;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range:not(.min):not(.max) .rangeNub) {
		border-radius: 10em 10em 10em 1.6em;
	}
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range .rangeHandle:nth-of-type(1) .rangeNub
	) {
		transform: rotate(-135deg);
	}
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range .rangeHandle:nth-of-type(2) .rangeNub
	) {
		transform: rotate(45deg);
	}
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.reversed
			.rangeHandle:nth-of-type(1)
			.rangeNub
	) {
		transform: rotate(45deg);
	}
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.reversed
			.rangeHandle:nth-of-type(2)
			.rangeNub
	) {
		transform: rotate(-135deg);
	}
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical
			.rangeHandle:nth-of-type(1)
			.rangeNub
	) {
		transform: rotate(135deg);
	}
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical
			.rangeHandle:nth-of-type(2)
			.rangeNub
	) {
		transform: rotate(-45deg);
	}
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical.reversed
			.rangeHandle:nth-of-type(1)
			.rangeNub
	) {
		transform: rotate(-45deg);
	}
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical.reversed
			.rangeHandle:nth-of-type(2)
			.rangeNub
	) {
		transform: rotate(135deg);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeFloat) {
		display: block;
		position: absolute;
		left: 50%;
		top: -0.5em;
		transform: translate(-50%, -100%);
		font-size: 1em;
		text-align: center;
		opacity: 0;
		pointer-events: none;
		white-space: nowrap;
		transition: all 0.2s ease;
		font-size: 0.9em;
		padding: 0.2em 0.4em;
		border-radius: 0.2em;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle.active .rangeFloat),
	:global(
		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable .rangeHandle:hover .rangeFloat
	) {
		opacity: 1;
		top: -0.2em;
		transform: translate(-50%, -100%);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeBar) {
		position: absolute;
		display: block;
		transition: background 0.2s ease;
		border-radius: 1em;
		height: 0.5em;
		top: 0;
		user-select: none;
		z-index: 1;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical .rangeBar) {
		width: 0.5em;
		height: auto;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28) {
		background-color: var(--slider, #d7dada);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeBar) {
		background-color: var(--range-inactive, #99a2a2);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.focus .rangeBar) {
		background-color: var(--range, #838de7);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeNub) {
		background-color: var(--handle-inactive, #99a2a2);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.focus .rangeNub) {
		background-color: var(--handle, #838de7);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle.active .rangeNub) {
		background-color: var(--handle-focus, #4a40d4);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeFloat) {
		color: white;
		color: var(--float-text);
		background-color: var(--float-inactive, #99a2a2);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.focus .rangeFloat) {
		background-color: var(--float, #4a40d4);
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.disabled) {
		opacity: 0.5;
	}
	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.disabled .rangeNub) {
		background-color: var(--slider, #d7dada);
	}
</style>
