useDragGesture

The drag gesture behind Slider, RangeSlider, Joystick and Rating: spread its handlers, ref and style onto a View to get pointer samples in that View's own coordinates, which keep tracking after the finger leaves it, without the page or a ScrollView stealing the drag. getGestureSurfaceStyle and GESTURE_RESPONDER_LOCK are the two pieces it's built on, for a PanResponder of your own.
useDragGesture(options) returns { ref, onLayout, surfaceStyle, panHandlers, isDragging, getSurfaceRect }; put the first four on the same View (composing onLayout when you need your own) and give its children pointerEvents: 'none' so every press lands on the surface. onStart, onMove and onEnd each receive a DragPoint with x / y relative to the surface, its width / height (0 until the first layout) and dx / dy / distance travelled since the press, while onCancel fires instead of onEnd when the gesture is taken away. axis sets web touch-action: 'x' here leaves vertical page scrolling alone, 'both' (the default) claims every direction, and on web a drag also holds page-scroll and text-selection locks until release, cancel or unmount. claimOnStart: false with an activationDistance lets a tap fall through to something else, and enabled: false turns the surface back into ordinary content.

Loading demo…

import { useState } from 'react'; import { Block, Text, useDragGesture } from '@plocks/ui'; import type { DragPoint } from '@plocks/ui'; const KNOB = 28; // Children of the surface let the press through, so it always lands on the surface itself. const PASS_THROUGH = { pointerEvents: 'none' } as const; export function Demo() { const [ratio, setRatio] = useState(0.5); const [trackWidth, setTrackWidth] = useState(0); const follow = (point: DragPoint) => { const travel = point.width - KNOB; if (travel <= 0) return; setRatio(Math.min(1, Math.max(0, (point.x - KNOB / 2) / travel))); }; const drag = useDragGesture({ axis: 'x', cursor: 'grab', activeCursor: 'grabbing', onStart: follow, onMove: follow, }); return ( <Block fullWidth maw={360}> <Block ref={drag.ref} onLayout={(event) => { drag.onLayout(event); setTrackWidth(event.nativeEvent.layout.width); }} style={drag.surfaceStyle} {...drag.panHandlers} h={KNOB} justify="center" > <Block h={4} radius="full" bg="border" style={PASS_THROUGH} /> <Block position="absolute" top={0} left={ratio * Math.max(0, trackWidth - KNOB)} w={KNOB} h={KNOB} radius="full" bg="primary.5" opacity={drag.isDragging ? 0.8 : 1} style={PASS_THROUGH} /> </Block> <Text ta="center" ff="monospace"> {Math.round(ratio * 100)}% </Text> </Block> ); }