react-native-step-slider 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ismnoiet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+
2
+ <div align="center">
3
+
4
+ <table>
5
+ <tr>
6
+ <th align="center">iOS</th>
7
+ <th align="center">Android</th>
8
+ </tr>
9
+ <tr>
10
+ <td align="center">
11
+ <img src="https://github.com/ismnoiet/detailed-react-native-dot-slider/blob/main/step-slider-ios-demos.gif" width="300" height="700" alt="iOS demo" />
12
+ </td>
13
+ <td align="center">
14
+ <img src="https://github.com/ismnoiet/detailed-react-native-dot-slider/blob/main/step-slider-android-demos.gif" width="300" height="700" alt="Android demo" />
15
+ </td>
16
+ </tr>
17
+ </table>
18
+
19
+
20
+ <h1>react-native-step-slider</h1>
21
+
22
+ <p>A beautifully animated step slider for React Native — with spring snap and tap-to-select gestures.</p>
23
+
24
+ <p>
25
+ <img src="https://img.shields.io/npm/v/react-native-step-slider.svg?style=flat-square" alt="Version" />
26
+ <img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License" />
27
+ <img src="https://img.shields.io/badge/platform-iOS%20%7C%20Android-blue.svg" alt="Platform" />
28
+ <img src="https://img.shields.io/badge/PRs-welcome-purple.svg" alt="PRs Welcome" />
29
+ </p>
30
+
31
+ </div>
32
+
33
+ ---
34
+
35
+ ## ✨ Features
36
+
37
+ - 📱 **Works on iOS & Android** — consistent behaviour and look across both platforms
38
+ - 🎨 **Fully customisable** — colours, dot count, size and layout
39
+ - ⚡ **Silky smooth performance** — animations and gestures never block your UI
40
+ - 🎯 **Tap to jump** — tap any dot to jump straight to that step
41
+ - 🟣 **Active dot pulse** — the selected dot pops with a springy highlight
42
+ - 🌊 **Progress fill** — a live fill tracks your position across the slider
43
+
44
+ ---
45
+
46
+ ## Installation
47
+
48
+ ```sh
49
+ npm install react-native-step-slider
50
+ # or
51
+ yarn add react-native-step-slider
52
+ ```
53
+
54
+ ### Peer dependencies
55
+
56
+ Install all three peer packages if they aren't already in your project:
57
+
58
+ ```sh
59
+ npm install @shopify/react-native-skia react-native-reanimated react-native-gesture-handler
60
+ ```
61
+
62
+ | Package | Minimum version |
63
+ |---|---|
64
+ | `@shopify/react-native-skia` | `>= 1.0.0` |
65
+ | `react-native-reanimated` | `>= 3.0.0` |
66
+ | `react-native-gesture-handler` | `>= 2.0.0` |
67
+ | `react` | `>= 18` |
68
+ | `react-native` | `>= 0.72` |
69
+
70
+ ### iOS
71
+
72
+ ```sh
73
+ cd ios && pod install
74
+ ```
75
+
76
+ ### Babel config
77
+
78
+ Make sure `react-native-reanimated/plugin` is the **last** item in your `babel.config.js` plugins array:
79
+
80
+ ```js
81
+ // babel.config.js
82
+ module.exports = {
83
+ presets: ['module:@react-native/babel-preset'],
84
+ plugins: ['react-native-reanimated/plugin'],
85
+ };
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Quick start
91
+
92
+ Wrap your app in `GestureHandlerRootView` (once, at the root) and drop in `DotSlider`:
93
+
94
+ ```tsx
95
+ import React, { useState } from 'react';
96
+ import { Text, View } from 'react-native';
97
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
98
+ import { DotSlider } from 'react-native-step-slider';
99
+
100
+ export default function App() {
101
+ const [step, setStep] = useState(5);
102
+
103
+ return (
104
+ <GestureHandlerRootView style={{ flex: 1 }}>
105
+ <View style={{ padding: 32 }}>
106
+ <Text>Step {step + 1} of 11</Text>
107
+ <DotSlider dotCount={11} defaultIndex={5} onValueChange={setStep} />
108
+ </View>
109
+ </GestureHandlerRootView>
110
+ );
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Props
117
+
118
+ | Prop | Type | Default | Description |
119
+ |---|---|---|---|
120
+ | `dotCount` | `number` | `11` | Number of selectable steps. |
121
+ | `defaultIndex` | `number` | `Math.floor(dotCount / 2)` | Initially selected dot index (0-based). |
122
+ | `width` | `number` | `screenWidth − 64` | Total width of the slider track in dp. |
123
+ | `trackHeight` | `number` | `56` | Height of the pill-shaped track in dp. |
124
+ | `colors` | `DotSliderColors` | *(blue theme)* | Override any or all colour tokens. |
125
+ | `onValueChange` | `(index: number) => void` | `undefined` | Called every time the selected index changes. |
126
+
127
+ ---
128
+
129
+ ## DotSliderColors
130
+
131
+ All fields are optional — only provide the tokens you want to override.
132
+
133
+ | Field | Default | Description |
134
+ |---|---|---|
135
+ | `track` | `'#e8f0fe'` | Track background fill. |
136
+ | `fill` | `'#4f86f7'` | Progress fill (left of thumb). |
137
+ | `dotActive` | `'#1a56f0'` | Selected dot colour. |
138
+ | `dotInactive` | `'#a8c3fa'` | Unselected dot colour. |
139
+ | `thumb` | `'#1a56f0'` | Thumb pill colour. |
140
+ | `thumbShadow` | `'rgba(26,86,240,0.4)'` | Thumb drop-shadow colour. |
141
+
142
+ ```tsx
143
+ <DotSlider
144
+ dotCount={7}
145
+ colors={{
146
+ track: '#fdf2f8',
147
+ fill: '#fbcfe8',
148
+ dotActive: '#ec4899',
149
+ dotInactive: '#f9a8d4',
150
+ thumb: '#ec4899',
151
+ thumbShadow: 'rgba(236,72,153,0.45)',
152
+ }}
153
+ onValueChange={(i) => console.log('selected:', i)}
154
+ />
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Example app
160
+
161
+ A runnable demo is in [`Demos.tsx`](./Demos.tsx) at the root of the repo. It shows a wide variety of real-world use cases — volume, brightness, font size, filters, ratings and more.
162
+
163
+ ---
164
+
165
+ ## How it works
166
+
167
+ The slider is rendered on a single hardware-accelerated canvas, so every dot, the progress fill and the animated thumb are drawn in one efficient pass — no layout thrashing. Animations run on a dedicated UI thread, meaning dragging and snapping stay perfectly smooth even when your JavaScript is busy. The `onValueChange` callback is only fired when the selected step actually changes, keeping unnecessary re-renders to a minimum.
168
+
169
+ ---
170
+
171
+ ## Contributing
172
+
173
+ Contributions are welcome! Please open an issue first to discuss what you'd like to change.
174
+
175
+ ---
176
+
177
+ ## License
178
+
179
+ MIT © [ismnoiet](https://github.com/ismnoiet) — see [LICENSE](./LICENSE) for details.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "react-native-step-slider",
3
+ "version": "1.0.0",
4
+ "description": "A smooth, customisable step slider for React Native with spring snap, animated progress fill and tap-to-select.",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "files": [
8
+ "src/"
9
+ ],
10
+ "keywords": [
11
+ "react-native",
12
+ "slider",
13
+ "step",
14
+ "stepper",
15
+ "steps",
16
+ "dot",
17
+ "dots",
18
+ "smooth",
19
+ "interactive",
20
+ "skia",
21
+ "reanimated",
22
+ "gesture-handler",
23
+ "animation",
24
+ "component"
25
+ ],
26
+ "author": "ismnoiet",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/ismnoiet/react-native-step-slider.git"
31
+ },
32
+ "peerDependencies": {
33
+ "@shopify/react-native-skia": ">=1.0.0",
34
+ "react": ">=18.0.0",
35
+ "react-native": ">=0.72.0",
36
+ "react-native-gesture-handler": ">=2.0.0",
37
+ "react-native-reanimated": ">=3.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/react": "^18.0.0",
41
+ "@types/react-native": "^0.73.0",
42
+ "typescript": "^5.0.0"
43
+ }
44
+ }
@@ -0,0 +1,400 @@
1
+ /**
2
+ * DotSlider
3
+ *
4
+ * A smooth, fully Skia-rendered horizontal slider with dot step markers,
5
+ * animated progress fill, spring snap, and tap-to-select.
6
+ *
7
+ * Peer dependencies:
8
+ * @shopify/react-native-skia >= 1.0.0
9
+ * react-native-reanimated >= 3.0.0 | 4.x
10
+ * react-native-gesture-handler >= 2.0.0
11
+ */
12
+
13
+ import React from 'react';
14
+ import { Dimensions, StyleSheet, View } from 'react-native';
15
+
16
+ import {
17
+ Canvas,
18
+ Circle,
19
+ Group,
20
+ RoundedRect,
21
+ Shadow,
22
+ rect,
23
+ rrect,
24
+ } from '@shopify/react-native-skia';
25
+
26
+ import {
27
+ Easing,
28
+ interpolate,
29
+ runOnJS,
30
+ useDerivedValue,
31
+ useSharedValue,
32
+ withSpring,
33
+ withTiming,
34
+ } from 'react-native-reanimated';
35
+
36
+ import {
37
+ Gesture,
38
+ GestureDetector,
39
+ } from 'react-native-gesture-handler';
40
+
41
+ // ─── Types ────────────────────────────────────────────────────────────────────
42
+
43
+ export interface DotSliderColors {
44
+ /** Background colour of the pill track. @default '#dbeafe' */
45
+ track?: string;
46
+ /** Colour of the progress fill (left of thumb). @default '#bfdbfe' */
47
+ fill?: string;
48
+ /** Active dot colour (left of / at thumb). @default '#3b82f6' */
49
+ dotActive?: string;
50
+ /** Inactive dot colour (right of thumb). @default '#93c5fd' */
51
+ dotInactive?: string;
52
+ /** Thumb colour. @default '#3b82f6' */
53
+ thumb?: string;
54
+ /** Thumb shadow colour. @default 'rgba(59,130,246,0.5)' */
55
+ thumbShadow?: string;
56
+ }
57
+
58
+ export interface DotSliderProps {
59
+ /**
60
+ * Number of selectable dot positions.
61
+ * Must be >= 2. Odd values place a dot at the exact centre.
62
+ * @default 11
63
+ */
64
+ dotCount?: number;
65
+
66
+ /**
67
+ * Zero-based index of the initially selected dot.
68
+ * Clamped to [0, dotCount - 1].
69
+ * @default Math.floor(dotCount / 2)
70
+ */
71
+ defaultIndex?: number;
72
+
73
+ /**
74
+ * Width of the slider track in dp.
75
+ * Defaults to screen width minus 64 dp of horizontal padding.
76
+ */
77
+ width?: number;
78
+
79
+ /** Height of the pill track in dp. @default 56 */
80
+ trackHeight?: number;
81
+
82
+ /**
83
+ * Corner radius of the track.
84
+ * @default trackHeight / 2 (full pill / stadium shape)
85
+ */
86
+ trackRadius?: number;
87
+
88
+ /**
89
+ * Radius of each dot in dp.
90
+ * The active dot pulses up to 1.45× this value.
91
+ * @default 3.5
92
+ */
93
+ dotRadius?: number;
94
+
95
+ /**
96
+ * Width of the thumb pill in dp.
97
+ * @default 10
98
+ */
99
+ thumbWidth?: number;
100
+
101
+ /**
102
+ * Height of the thumb pill in dp.
103
+ * Defaults to 57 % of trackHeight.
104
+ */
105
+ thumbHeight?: number;
106
+
107
+ /**
108
+ * Show the gloss sheen overlay on the thumb.
109
+ * @default true
110
+ */
111
+ showThumbGloss?: boolean;
112
+
113
+ /**
114
+ * Extra space (in dp) between the track's left edge and the first dot.
115
+ * Defaults to the track corner radius so the first dot sits visually
116
+ * inside the pill.
117
+ */
118
+ dotPaddingStart?: number;
119
+
120
+ /**
121
+ * Extra space (in dp) between the last dot and the track's right edge.
122
+ * Defaults to the track corner radius so the last dot sits visually
123
+ * inside the pill.
124
+ */
125
+ dotPaddingEnd?: number;
126
+
127
+ /** Colour overrides — any unset key falls back to its default. */
128
+ colors?: DotSliderColors;
129
+
130
+ /**
131
+ * Called whenever the selected dot index changes (0-based).
132
+ * Fired on gesture end after the spring snap target is determined.
133
+ */
134
+ onValueChange?: (index: number) => void;
135
+ }
136
+
137
+ // ─── SliderDot (internal sub-component) ──────────────────────────────────────
138
+
139
+ interface SliderDotProps {
140
+ cx: number;
141
+ cy: number;
142
+ dotStep: number;
143
+ dotRadius: number;
144
+ thumbX: ReturnType<typeof useSharedValue<number>>;
145
+ colorActive: string;
146
+ colorInactive: string;
147
+ }
148
+
149
+ function SliderDot({
150
+ cx,
151
+ cy,
152
+ dotStep,
153
+ dotRadius,
154
+ thumbX,
155
+ colorActive,
156
+ colorInactive,
157
+ }: SliderDotProps) {
158
+ const color = useDerivedValue(() =>
159
+ cx <= thumbX.value ? colorActive : colorInactive,
160
+ );
161
+
162
+ const r = useDerivedValue(() => {
163
+ const dist = Math.abs(cx - thumbX.value);
164
+ const pulse = interpolate(dist, [0, dotStep], [1.3, 1.0], 'clamp');
165
+ return dotRadius * pulse;
166
+ });
167
+
168
+ return <Circle cx={cx} cy={cy} r={r} color={color} />;
169
+ }
170
+
171
+ // ─── DotSlider ────────────────────────────────────────────────────────────────
172
+
173
+ export function DotSlider({
174
+ dotCount = 11,
175
+ defaultIndex,
176
+ width,
177
+ trackHeight = 56,
178
+ trackRadius,
179
+ dotRadius = 3.5,
180
+ thumbWidth,
181
+ thumbHeight,
182
+ showThumbGloss = true,
183
+ dotPaddingStart,
184
+ dotPaddingEnd,
185
+ colors = {},
186
+ onValueChange,
187
+ }: DotSliderProps) {
188
+ // ── Layout ─────────────────────────────────────────────────────────────────
189
+
190
+ const { width: SW } = Dimensions.get('window');
191
+ const TRACK_W = width ?? SW - 64;
192
+ const TRACK_H = trackHeight;
193
+ const TRACK_R = trackRadius ?? TRACK_H / 2;
194
+ const CANVAS_H = TRACK_H + 32;
195
+ const CY = CANVAS_H / 2;
196
+
197
+ const THUMB_W = thumbWidth ?? 7;
198
+ const THUMB_H = thumbHeight ?? Math.round(TRACK_H * 0.62);
199
+ const THUMB_R = THUMB_W / 2;
200
+
201
+ const PAD_START = dotPaddingStart ?? TRACK_R;
202
+ const PAD_END = dotPaddingEnd ?? TRACK_R;
203
+ const N = Math.max(2, dotCount);
204
+ const DOT_AREA = TRACK_W - PAD_START - PAD_END;
205
+ const DOT_STEP = DOT_AREA / (N - 1);
206
+ const DOT_XS = Array.from({ length: N }, (_, i) => PAD_START + i * DOT_STEP);
207
+
208
+ const THUMB_MIN = DOT_XS[0];
209
+ const THUMB_MAX = DOT_XS[N - 1];
210
+
211
+ const initIdx = defaultIndex !== undefined
212
+ ? Math.max(0, Math.min(N - 1, defaultIndex))
213
+ : Math.floor(N / 2);
214
+ const INIT_X = DOT_XS[initIdx];
215
+
216
+ // ── Colours ────────────────────────────────────────────────────────────────
217
+
218
+ const C = {
219
+ track: colors.track ?? '#dbeafe',
220
+ fill: colors.fill ?? '#bfdbfe',
221
+ dotActive: colors.dotActive ?? '#3b82f6',
222
+ dotInactive: colors.dotInactive ?? '#93c5fd',
223
+ thumb: colors.thumb ?? '#3b82f6',
224
+ thumbShadow: colors.thumbShadow ?? 'rgba(59,130,246,0.5)',
225
+ };
226
+
227
+ // Pill clip path (keeps progress fill inside the track boundary)
228
+ const pillClip = rrect(rect(0, CY - TRACK_H / 2, TRACK_W, TRACK_H), TRACK_R, TRACK_R);
229
+
230
+ // ── Shared values ──────────────────────────────────────────────────────────
231
+
232
+ const thumbX = useSharedValue(INIT_X);
233
+ const dragging = useSharedValue(0);
234
+ const startX = useSharedValue(INIT_X);
235
+
236
+ // ── Derived values ─────────────────────────────────────────────────────────
237
+
238
+ const thumbScaleY = useDerivedValue(() =>
239
+ interpolate(dragging.value, [0, 1], [1.0, 1.08]),
240
+ );
241
+ const thumbScaleX = useDerivedValue(() =>
242
+ interpolate(dragging.value, [0, 1], [1.0, 0.88]),
243
+ );
244
+
245
+ const thumbTransform = useDerivedValue(() => [
246
+ { translateX: thumbX.value },
247
+ { translateY: CY },
248
+ { scaleX: thumbScaleX.value },
249
+ { scaleY: thumbScaleY.value },
250
+ { translateX: -thumbX.value },
251
+ { translateY: -CY },
252
+ ]);
253
+
254
+ const progressW = useDerivedValue(() =>
255
+ Math.min(thumbX.value + THUMB_W / 2 + DOT_STEP / 2, TRACK_W + TRACK_R),
256
+ );
257
+
258
+ const thumbRectX = useDerivedValue(() => thumbX.value - THUMB_W / 2);
259
+ const thumbRectY = useDerivedValue(() => CY - THUMB_H / 2);
260
+ const shadowBlur = useDerivedValue(() =>
261
+ interpolate(dragging.value, [0, 1], [3, 10]),
262
+ );
263
+
264
+ // ── Snap helper (worklet) ──────────────────────────────────────────────────
265
+
266
+ const snapNearest = (x: number): number => {
267
+ 'worklet';
268
+ let best = DOT_XS[0];
269
+ let bestD = Math.abs(x - best);
270
+ for (let i = 1; i < DOT_XS.length; i++) {
271
+ const d = Math.abs(x - DOT_XS[i]);
272
+ if (d < bestD) { bestD = d; best = DOT_XS[i]; }
273
+ }
274
+ return best;
275
+ };
276
+
277
+ const notifyChange = (snappedX: number) => {
278
+ const idx = DOT_XS.findIndex(x => Math.abs(x - snappedX) < 0.5);
279
+ if (idx !== -1) onValueChange?.(idx);
280
+ };
281
+
282
+ // ── Gesture ────────────────────────────────────────────────────────────────
283
+
284
+ const pan = Gesture.Pan()
285
+ .minDistance(0)
286
+ .onBegin(() => {
287
+ 'worklet';
288
+ startX.value = thumbX.value;
289
+ dragging.value = withTiming(1, { duration: 100, easing: Easing.out(Easing.quad) });
290
+ })
291
+ .onUpdate((e: { translationX: number }) => {
292
+ 'worklet';
293
+ thumbX.value = Math.max(THUMB_MIN, Math.min(THUMB_MAX, startX.value + e.translationX));
294
+ })
295
+ .onEnd(() => {
296
+ 'worklet';
297
+ dragging.value = withTiming(0, { duration: 200 });
298
+ const nearest = snapNearest(thumbX.value);
299
+ thumbX.value = withSpring(nearest, { damping: 28, stiffness: 320, mass: 0.5 });
300
+ runOnJS(notifyChange)(nearest);
301
+ });
302
+
303
+ const tap = Gesture.Tap()
304
+ .onEnd((e: { x: number }) => {
305
+ 'worklet';
306
+ const nearest = snapNearest(e.x);
307
+ dragging.value = withTiming(1, { duration: 80 });
308
+ thumbX.value = withSpring(
309
+ nearest,
310
+ { damping: 28, stiffness: 320, mass: 0.5 },
311
+ () => { dragging.value = withTiming(0, { duration: 200 }); },
312
+ );
313
+ runOnJS(notifyChange)(nearest);
314
+ });
315
+
316
+ const gesture = Gesture.Race(tap, pan);
317
+
318
+ // ── Render ─────────────────────────────────────────────────────────────────
319
+
320
+ return (
321
+ <GestureDetector gesture={gesture}>
322
+ <View style={styles.wrap}>
323
+ <Canvas style={{ width: TRACK_W, height: CANVAS_H }}>
324
+
325
+ {/* Track background with inset shadow */}
326
+ <RoundedRect
327
+ x={0} y={CY - TRACK_H / 2}
328
+ width={TRACK_W} height={TRACK_H}
329
+ r={TRACK_R} color={C.track}
330
+ >
331
+ <Shadow dx={0} dy={1} blur={3} color="rgba(0,0,0,0.08)" inner />
332
+ </RoundedRect>
333
+
334
+ {/* Progress fill — clipped to pill */}
335
+ <Group clip={pillClip}>
336
+ <RoundedRect
337
+ x={0} y={CY - TRACK_H / 2}
338
+ width={progressW} height={TRACK_H}
339
+ r={TRACK_R} color={C.fill}
340
+ />
341
+ </Group>
342
+
343
+ {/* Dots */}
344
+ {DOT_XS.map(cx => (
345
+ <SliderDot
346
+ key={cx}
347
+ cx={cx}
348
+ cy={CY}
349
+ dotStep={DOT_STEP}
350
+ dotRadius={dotRadius}
351
+ thumbX={thumbX}
352
+ colorActive={C.dotActive}
353
+ colorInactive={C.dotInactive}
354
+ />
355
+ ))}
356
+
357
+ {/* Thumb */}
358
+ <Group transform={thumbTransform}>
359
+ {/* Shadow layer */}
360
+ <RoundedRect
361
+ x={thumbRectX} y={thumbRectY}
362
+ width={THUMB_W} height={THUMB_H}
363
+ r={THUMB_R} color="transparent"
364
+ >
365
+ <Shadow dx={0} dy={1} blur={shadowBlur} color={C.thumbShadow} />
366
+ </RoundedRect>
367
+ {/* Body */}
368
+ <RoundedRect
369
+ x={thumbRectX} y={thumbRectY}
370
+ width={THUMB_W} height={THUMB_H}
371
+ r={THUMB_R} color={C.thumb}
372
+ />
373
+ {/* Gloss — thin top-edge strip only */}
374
+ {showThumbGloss && (
375
+ <RoundedRect
376
+ x={thumbRectX} y={thumbRectY}
377
+ width={THUMB_W} height={THUMB_H * 0.28}
378
+ r={THUMB_R} color="rgba(255,255,255,0.12)"
379
+ />
380
+ )}
381
+ </Group>
382
+
383
+ </Canvas>
384
+ </View>
385
+ </GestureDetector>
386
+ );
387
+ }
388
+
389
+ // ─── Styles ───────────────────────────────────────────────────────────────────
390
+
391
+ const styles = StyleSheet.create({
392
+ wrap: {
393
+ alignItems: 'center',
394
+ shadowColor: '#000',
395
+ shadowOffset: { width: 0, height: 1 },
396
+ shadowOpacity: 0.06,
397
+ shadowRadius: 4,
398
+ elevation: 2,
399
+ },
400
+ });
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { DotSlider } from './DotSlider';
2
+ export type { DotSliderProps, DotSliderColors } from './DotSlider';