Skip to content

Commit 88fc788

Browse files
committed
cooler slider
1 parent 5f048e1 commit 88fc788

File tree

2 files changed

+71
-92
lines changed

2 files changed

+71
-92
lines changed

apps/dashboard/components/ui/elastic-slider.tsx

Lines changed: 69 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
useMotionValueEvent,
88
useTransform,
99
} from 'motion/react';
10-
import { useEffect, useRef, useState } from 'react';
10+
import { useCallback, useRef, useState } from 'react';
1111
import { cn } from '@/lib/utils';
1212

1313
const MAX_OVERFLOW = 30;
@@ -25,6 +25,13 @@ interface SliderProps {
2525
disabled?: boolean;
2626
}
2727

28+
function decay(value: number, maxValue: number): number {
29+
if (maxValue === 0) return 0;
30+
const entry = value / maxValue;
31+
const sigmoid = 2 * (1 / (1 + Math.exp(-entry)) - 0.5);
32+
return sigmoid * maxValue;
33+
}
34+
2835
export function Slider({
2936
value = 0,
3037
onValueChange,
@@ -45,84 +52,75 @@ export function Slider({
4552
const clientX = useMotionValue(0);
4653
const overflow = useMotionValue(0);
4754

48-
useEffect(() => {
49-
setInternalValue(value);
50-
}, [value]);
55+
const percentage = ((internalValue - min) / (max - min || 1)) * 100;
5156

5257
useMotionValueEvent(clientX, 'change', (latest: number) => {
53-
if (sliderRef.current && isDragging) {
54-
const { left, right } = sliderRef.current.getBoundingClientRect();
55-
let newOverflow: number;
56-
57-
if (latest < left) {
58-
setRegion('left');
59-
newOverflow = left - latest;
60-
} else if (latest > right) {
61-
setRegion('right');
62-
newOverflow = latest - right;
63-
} else {
64-
setRegion('middle');
65-
newOverflow = 0;
66-
}
67-
68-
overflow.jump(decay(newOverflow, MAX_OVERFLOW));
58+
if (!sliderRef.current || !isDragging) return;
59+
60+
const { left, right } = sliderRef.current.getBoundingClientRect();
61+
let newOverflow = 0;
62+
63+
if (latest < left) {
64+
setRegion('left');
65+
newOverflow = left - latest;
66+
} else if (latest > right) {
67+
setRegion('right');
68+
newOverflow = latest - right;
69+
} else {
70+
setRegion('middle');
6971
}
70-
});
7172

72-
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
73-
if (!isDragging || disabled || !sliderRef.current) {
74-
return;
75-
}
73+
overflow.jump(decay(newOverflow, MAX_OVERFLOW));
74+
});
7675

77-
const { left, width } = sliderRef.current.getBoundingClientRect();
78-
let newValue = min + ((e.clientX - left) / width) * (max - min);
76+
const updateValue = useCallback(
77+
(clientXPos: number) => {
78+
if (!sliderRef.current) return;
7979

80-
// Apply step
81-
if (step > 0) {
82-
newValue = Math.round(newValue / step) * step;
83-
}
80+
const { left, width } = sliderRef.current.getBoundingClientRect();
81+
let newValue = min + ((clientXPos - left) / width) * (max - min);
8482

85-
// Clamp to bounds
86-
newValue = Math.min(Math.max(newValue, min), max);
83+
if (step > 0) {
84+
newValue = Math.round(newValue / step) * step;
85+
}
8786

88-
setInternalValue(newValue);
89-
onValueChange?.(newValue);
90-
clientX.jump(e.clientX);
91-
};
87+
newValue = Math.min(Math.max(newValue, min), max);
88+
setInternalValue(newValue);
89+
onValueChange?.(newValue);
90+
clientX.jump(clientXPos);
91+
},
92+
[min, max, step, onValueChange, clientX]
93+
);
9294

9395
const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
94-
if (disabled) {
95-
return;
96-
}
96+
if (disabled) return;
9797

9898
setIsDragging(true);
99-
handlePointerMove(e);
99+
updateValue(e.clientX);
100100
e.currentTarget.setPointerCapture(e.pointerId);
101+
document.body.style.cursor = 'grabbing';
102+
};
103+
104+
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
105+
if (!isDragging || disabled) return;
106+
updateValue(e.clientX);
101107
};
102108

103109
const handlePointerUp = () => {
104110
setIsDragging(false);
105111
setRegion('middle');
106112
overflow.jump(0);
107-
};
108-
109-
const getPercentage = (): number => {
110-
const range = max - min;
111-
if (range === 0) {
112-
return 0;
113-
}
114-
return ((internalValue - min) / range) * 100;
113+
document.body.style.cursor = '';
115114
};
116115

117116
return (
118117
<div className={cn('space-y-3', className)}>
119118
<div
120119
className={cn(
121-
'flex items-center gap-4',
120+
'flex select-none items-center gap-4',
122121
disabled && 'cursor-not-allowed opacity-50'
123122
)}
124123
>
125-
{/* Left Icon */}
126124
<motion.div
127125
className="shrink-0 text-muted-foreground"
128126
style={{
@@ -135,62 +133,54 @@ export function Slider({
135133
{leftIcon}
136134
</motion.div>
137135

138-
{/* Slider Track */}
139136
<div
137+
ref={sliderRef}
140138
className={cn(
141-
'relative flex-1 cursor-pointer touch-none',
142-
disabled && 'cursor-not-allowed'
139+
'relative flex-1 touch-none',
140+
disabled ? 'cursor-not-allowed' : 'cursor-grab',
141+
isDragging && 'cursor-grabbing'
143142
)}
144143
onPointerDown={handlePointerDown}
145-
onPointerLeave={handlePointerUp}
146144
onPointerMove={handlePointerMove}
147145
onPointerUp={handlePointerUp}
148-
ref={sliderRef}
146+
onPointerLeave={handlePointerUp}
149147
>
150148
<motion.div
151149
className="relative"
152150
style={{
153151
scaleX: useTransform(() => {
154-
if (sliderRef.current) {
155-
const { width } = sliderRef.current.getBoundingClientRect();
156-
return 1 + overflow.get() / width;
157-
}
158-
return 1;
152+
if (!sliderRef.current) return 1;
153+
const { width } = sliderRef.current.getBoundingClientRect();
154+
return 1 + overflow.get() / width;
159155
}),
160156
scaleY: useTransform(overflow, [0, MAX_OVERFLOW], [1, 0.7]),
161157
transformOrigin: useTransform(() => {
162-
if (sliderRef.current) {
163-
const { left, width } =
164-
sliderRef.current.getBoundingClientRect();
165-
return clientX.get() < left + width / 2 ? 'right' : 'left';
166-
}
167-
return 'center';
158+
if (!sliderRef.current) return 'center';
159+
const { left, width } =
160+
sliderRef.current.getBoundingClientRect();
161+
return clientX.get() < left + width / 2 ? 'right' : 'left';
168162
}),
169163
}}
170164
>
171-
{/* Track Background */}
172165
<div className="h-2 w-full rounded-full bg-secondary">
173-
{/* Progress */}
174166
<div
175-
className="h-full rounded-full bg-primary"
176-
style={{ width: `${getPercentage()}%` }}
167+
className="h-full rounded-full bg-primary transition-[width] duration-75"
168+
style={{ width: `${percentage}%` }}
177169
/>
178170
</div>
179171

180-
{/* Thumb */}
181172
<motion.div
182-
className="-translate-y-1/2 absolute top-1/2 h-4 w-4 rounded-full border-2 border-primary bg-background shadow-sm"
183-
style={{
184-
left: `${getPercentage()}%`,
185-
x: '-50%',
186-
}}
173+
className={cn(
174+
'-translate-y-1/2 absolute top-1/2 size-4 rounded-full border-2 border-primary bg-background shadow-sm',
175+
isDragging ? 'cursor-grabbing' : 'cursor-grab'
176+
)}
177+
style={{ left: `${percentage}%`, x: '-50%' }}
187178
whileHover={{ scale: 1.1 }}
188179
whileTap={{ scale: 0.95 }}
189180
/>
190181
</motion.div>
191182
</div>
192183

193-
{/* Right Icon */}
194184
<motion.div
195185
className="shrink-0 text-muted-foreground"
196186
style={{
@@ -204,10 +194,9 @@ export function Slider({
204194
</motion.div>
205195
</div>
206196

207-
{/* Value Display */}
208197
{showValue && (
209198
<div className="text-center">
210-
<span className="font-medium font-mono text-sm">
199+
<span className="font-medium font-mono text-sm tabular-nums">
211200
{Math.round(internalValue)}
212201
{max === 100 && '%'}
213202
</span>
@@ -216,13 +205,3 @@ export function Slider({
216205
</div>
217206
);
218207
}
219-
220-
// Decay function for smooth overflow animation
221-
function decay(value: number, max: number): number {
222-
if (max === 0) {
223-
return 0;
224-
}
225-
const entry = value / max;
226-
const sigmoid = 2 * (1 / (1 + Math.exp(-entry)) - 0.5);
227-
return sigmoid * max;
228-
}

packages/mapper/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { umamiAdapter } from "./adapters";
1+
import { umamiAdapter as umami } from "./adapters";
22

33
export { mapEvents } from "./utils-map-events";
44

5-
export const adapters = { umami: umamiAdapter };
5+
export const adapters = { umami };
66
export type { AnalyticsEventAdapter } from "./types";

0 commit comments

Comments
 (0)