Overview
useMove enables pointer/touch scrubbing on an element and reports normalized coordinates. Values passed to onChange are clamped to 0..1; horizontal values are mirrored in rtl mode.
Installation
Import
import { useMove } from "@kuzenbo/hooks";
Usage
Basic Slider
import { useMove } from "@kuzenbo/hooks";
import { useState } from "react";
export function ColorSlider() {
const [value, setValue] = useState({ x: 0, y: 0 });
const { ref, active } = useMove(setValue);
return (
<div>
<div
ref={ref}
className="relative h-32 w-full bg-gradient-to-r from-blue-500 to-red-500 rounded-lg cursor-pointer"
>
<div
className="absolute h-4 w-4 bg-white rounded-full border-2 border-black transform -translate-x-1/2 -translate-y-1/2"
style={{
left: `${value.x * 100}%`,
top: `${value.y * 100}%`,
}}
/>
</div>
<p className="mt-2 text-sm">
Position: x={value.x.toFixed(2)}, y={value.y.toFixed(2)}
{active && " (scrubbing)"}
</p>
</div>
);
}
With Callbacks
import { useMove } from "@kuzenbo/hooks";
import { useState } from "react";
export function SliderWithCallbacks() {
const [value, setValue] = useState({ x: 0.5, y: 0.5 });
const [scrubbing, setScrubbing] = useState(false);
const { ref, active } = useMove(
setValue,
{
onScrubStart: () => setScrubbing(true),
onScrubEnd: () => setScrubbing(false),
}
);
return (
<div>
<div
ref={ref}
className="relative h-32 w-full bg-muted rounded-lg cursor-pointer"
>
<div
className="absolute h-3 w-3 bg-primary rounded-full transform -translate-x-1/2 -translate-y-1/2"
style={{
left: `${value.x * 100}%`,
top: `${value.y * 100}%`,
}}
/>
</div>
<p className="mt-2 text-sm">
{scrubbing ? "Scrubbing..." : "Ready"}
</p>
</div>
);
}
RTL Support
import { useMove } from "@kuzenbo/hooks";
import { useState } from "react";
export function RTLSlider() {
const [value, setValue] = useState({ x: 0, y: 0 });
const { ref } = useMove(setValue, undefined, "rtl");
return (
<div
ref={ref}
className="relative h-20 w-full bg-muted rounded-lg cursor-pointer"
dir="rtl"
>
<div
className="absolute h-3 w-3 bg-primary rounded-full transform -translate-x-1/2 -translate-y-1/2"
style={{
left: `${value.x * 100}%`,
top: `${value.y * 100}%`,
}}
/>
</div>
);
}
API Reference
function useMove<T extends HTMLElement = HTMLElement>(
onChange: (value: UseMovePosition) => void,
handlers?: UseMoveHandlers,
dir?: "ltr" | "rtl"
): UseMoveReturnValue<T>
onChange
(value: UseMovePosition) => void
required
Called on pointer movement with normalized { x, y } values (0..1)
Optional callbacks fired when scrubbing starts or endsCalled when scrubbing starts
Called when scrubbing ends
dir
'ltr' | 'rtl'
default:"ltr"
Horizontal direction used to interpret x values
Ref callback to attach to the scrubbing element
true when the user is currently scrubbing
Type Definitions
interface UseMovePosition {
x: number;
y: number;
}
interface UseMoveHandlers {
onScrubStart?: () => void;
onScrubEnd?: () => void;
}
interface UseMoveReturnValue<T extends HTMLElement = HTMLElement> {
ref: RefCallback<T | null>;
active: boolean;
}
Caveats
- Values are automatically clamped to
0..1 range
- In RTL mode, horizontal values are mirrored (1 becomes 0, 0 becomes 1)
- Uses
requestAnimationFrame for smooth updates
- Sets
user-select: none on the element during scrubbing
SSR and RSC Notes
- Use this hook in Client Components only
- Do not call it from React Server Components