@knitui/hooks
useRadialMove
Track pointer dragging over a circular control and report the angle (0–359) under the pointer. Web implementation — mirrors Mantine's useRadialMove. Wires pointerdown / pointermove / pointerup on window so the drag keeps following the cursor even when it leaves the ring, measuring the centre via getBoundingClientRect. The native counterpart lives in use-radial-move.native.ts; this file never imports react-native at runtime, so the web bundle stays free of it. onChange returns the value actually applied (after the consumer snaps / restricts it), which is forwarded to onChangeEnd when the gesture ends.
Import
import { useRadialMove } from "@knitui/hooks";Signature
export function useRadialMove( onChange: (angle: number) => number, options?: UseRadialMoveOptions, ): UseRadialMoveReturnuseRadialMove turns a drag over a round element into an angle. It measures the
element's centre, converts the pointer or finger position into degrees clockwise
from twelve o'clock, normalises the result into [0, 360), and calls your
onChange with it on press, on every move, and once more on release. It returns
a ref to put on the ring and a rootProps object to spread on it; AngleSlider
is built on it.
Your onChange returns a number — the value you actually applied after snapping
to marks or otherwise restricting the raw angle. That return value is what gets
forwarded to onChangeEnd, so the final value a form commits is the constrained
one rather than wherever the pointer happened to be lifted.
When to use it
- Angle and rotation controls: dials, knobs, gradient-angle pickers.
- Hue rings and other circular colour controls.
- Any radial scrubber where the position is meaningful but the distance from the centre is not.
Notes
- On the web only
pointerdownis bound to the element;pointermoveandpointerupgo onwindow, so the drag keeps tracking after the cursor leaves the ring. The bounding rect is measured once per gesture and invalidated on document scroll or window resize, because re-reading it insidepointermoveforces a layout flush on every event. - On native,
rootPropscarries React Native's gesture-responder handlers and must be spread onto the ring — therefalone does nothing there. It is an empty object on the web, so spread it unconditionally. The responder is claimed in the capture phase, before an ancestorScrollViewcan start scrolling, and termination requests are refused so the drag stays with the ring. - The native angle comes from window-absolute
pageX/pageYagainst the ring's measured origin, neverlocationX/locationY: those are relative to whichever child the touch is over, so the angle would jump as the finger crossed the handle. - Pointer and touch only —
AngleSlideradds its own arrow-key handling on top.disabledskips all handling, andonScrubStart/onScrubEndbracket the gesture (onScrubEndalso fires if the native responder is terminated).