expo-horizontal-picker 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,22 +1,126 @@
1
1
  # expo-horizontal-picker
2
2
 
3
- horizontal picker
3
+ A performant horizontal picker component for React Native and Expo apps.
4
+ - **Smooth Horizontal Scrolling**
5
+ Optimized with [`react-native-reanimated`](https://docs.expo.dev/versions/latest/sdk/reanimated/) for buttery-smooth, performant scroll animations.
4
6
 
5
- # API documentation
7
+ - **Snapping Behavior**
8
+ Automatically snaps to the closest item to give users a precise and polished interaction.
6
9
 
7
- - [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/horizontal-picker/)
8
- - [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/horizontal-picker/)
10
+ - **Fully Customizable**
11
+ Style any part of the picker — container, text, selected item styles — to fit your design system.
9
12
 
10
- # Installation in managed Expo projects
13
+ - **Initial Index Support**
14
+ Set the starting index to highlight a default item.
11
15
 
12
- For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
16
+ - **Built-in Haptic Feedback Support**
17
+ Easily integrate with [`expo-haptics`](https://docs.expo.dev/versions/latest/sdk/haptics/) to give subtle tactile feedback when items change.
13
18
 
14
- # Installation in bare React Native projects
19
+ - **TypeScript Support**
20
+ Fully typed API for a better developer experience.
15
21
 
16
- For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
22
+ - **Works with Expo and Bare React Native**
23
+ Supports both managed and bare workflows out of the box.
17
24
 
18
- ### Add the package to your npm dependencies
25
+ ## 📦 Installation
19
26
 
27
+ #### 1. Install the package
28
+ This package requires [`react-native-reanimated`](https://docs.expo.dev/versions/latest/sdk/reanimated/) to work:
29
+
30
+ ```bash
31
+ npm install expo-horizontal-picker react-native-reanimated
32
+ ```
33
+
34
+ Make sure to follow the additional setup instructions for Reanimated in the [official docs](https://docs.expo.dev/versions/latest/sdk/reanimated/#installation).
35
+
36
+ #### 2. (Optional) Install `expo-haptics` for haptic feedback
37
+
38
+ ```bash
39
+ npm install expo-haptics
40
+ ```
41
+
42
+ If you want to enable haptic feedback on item change, pass the `onHapticFeedback` prop and handle it using Expo Haptics:
43
+
44
+ ## 🎬 Demo
45
+
46
+ ![Example](./example.gif)
47
+
48
+ ```ts
49
+ import * as Haptics from 'expo-haptics';
50
+ import { HorizontalPicker } from 'expo-horizontal-picker';
51
+ import { SafeAreaView } from 'react-native';
52
+
53
+ export default function App() {
54
+ return (
55
+ <SafeAreaView style={styles.container}>
56
+ <View>
57
+ <HorizontalPicker
58
+ data={Array.from({ length: 100 }, (_, i) => ({
59
+ label: `${i + 1}`,
60
+ value: i + 1,
61
+ }))}
62
+ initialIndex={49}
63
+ visibleItemCount={7}
64
+ onHapticFeedback={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
65
+ />
66
+
67
+ <HorizontalPicker
68
+ data={Array.from({ length: 20 }, (_, i) => ({
69
+ label: `${i + 1}k`,
70
+ value: i + 1,
71
+ }))}
72
+ initialIndex={9}
73
+ visibleItemCount={5}
74
+ selectedItemTextStyle={{ color: 'orange', fontSize: 24 }}
75
+ onHapticFeedback={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Rigid)}
76
+ />
77
+
78
+ <HorizontalPicker
79
+ data={Array.from({ length: 24 }, (_, i) => ({
80
+ label: `${i + 1}h`,
81
+ value: i + 1,
82
+ }))}
83
+ initialIndex={11}
84
+ visibleItemCount={3}
85
+ itemTextStyle={{ fontSize: 20 }}
86
+ selectedItemTextStyle={{ fontSize: 40 }}
87
+ onHapticFeedback={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)}
88
+ />
89
+
90
+ <HorizontalPicker
91
+ data={Array.from({ length: 5 }, (_, i) => ({
92
+ label: `${(i + 1) * 10000}`,
93
+ value: (i + 1) * 10000,
94
+ }))}
95
+ initialIndex={2}
96
+ visibleItemCount={1}
97
+ itemTextStyle={{ fontSize: 60 }}
98
+ selectedItemTextStyle={{ fontSize: 60 }}
99
+ onHapticFeedback={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)}
100
+ />
101
+ </View>
102
+ </SafeAreaView>
103
+ );
104
+ }
105
+
106
+ const styles = {
107
+ container: {
108
+ flex: 1,
109
+ backgroundColor: '#eee',
110
+ },
111
+ };
20
112
  ```
21
- npm install expo-horizontal-picker
22
- ```
113
+
114
+ ## 🧩 Props
115
+ | Prop | Type | Default | Description |
116
+ |-------------------------|--------------------------------------------------------|---------|---------------------------------------------------------------------------------|
117
+ | `data` | `PickerOption[]` | – | Array of options to display. Each option is an object with `label` and `value`. |
118
+ | `initialIndex` | `number` | `0` | Index of the item initially selected. |
119
+ | `visibleItemCount` | `number` | `7` | Number of items visible on screen at once. |
120
+ | `onChange` | `(value: string \| number, index: number) => void` | – | Callback triggered when the selected item changes. |
121
+ | `onHapticFeedback` | `() => void` | – | Optional callback for haptic feedback when selection changes. |
122
+ | `containerStyle` | `AnimatedScrollViewProps['style']` | – | Style applied to the scroll container. |
123
+ | `itemContainerStyle` | `StyleProp<ViewStyle>` | – | Style applied to each item container. |
124
+ | `itemTextStyle` | `StyleProp<TextStyle>` | – | Style for unselected item text. |
125
+ | `selectedItemTextStyle` | `StyleProp<TextStyle>` | – | Style for selected item text. |
126
+ | ...props | `AnimatedScrollViewProps` | – | Additional props passed to `Animated.ScrollView`. |
package/build/index.d.ts CHANGED
@@ -4,7 +4,7 @@ interface PickerOption {
4
4
  label: string;
5
5
  value: string | number;
6
6
  }
7
- interface Props extends Omit<AnimatedScrollViewProps, 'style'> {
7
+ interface HorizontalPickerProps extends Omit<AnimatedScrollViewProps, 'style'> {
8
8
  data: PickerOption[];
9
9
  initialIndex?: number;
10
10
  visibleItemCount?: number;
@@ -15,6 +15,6 @@ interface Props extends Omit<AnimatedScrollViewProps, 'style'> {
15
15
  itemTextStyle?: StyleProp<TextStyle>;
16
16
  selectedItemTextStyle?: StyleProp<TextStyle>;
17
17
  }
18
- export declare function HorizontalPicker({ data, initialIndex, visibleItemCount, onChange, onHapticFeedback, containerStyle, itemContainerStyle, itemTextStyle, selectedItemTextStyle, ...props }: Props): import("react").JSX.Element;
18
+ export declare function HorizontalPicker({ data, initialIndex, visibleItemCount, onChange, onHapticFeedback, containerStyle, itemContainerStyle, itemTextStyle, selectedItemTextStyle, ...props }: HorizontalPickerProps): import("react").JSX.Element;
19
19
  export {};
20
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,SAAS,EAGd,KAAK,SAAS,EAEd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AACtB,OAAiB,EACf,KAAK,uBAAuB,EAM7B,MAAM,yBAAyB,CAAC;AAEjC,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAED,UAAU,KAAM,SAAQ,IAAI,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC5D,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,cAAc,CAAC,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAC;IAClD,kBAAkB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,qBAAqB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9C;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAgB,EAChB,gBAAoB,EACpB,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,qBAAqB,EACrB,GAAG,KAAK,EACT,EAAE,KAAK,+BAoHP"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,SAAS,EAGd,KAAK,SAAS,EAEd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AACtB,OAAiB,EACf,KAAK,uBAAuB,EAM7B,MAAM,yBAAyB,CAAC;AAEjC,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAED,UAAU,qBAAsB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC5E,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,cAAc,CAAC,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAC;IAClD,kBAAkB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,qBAAqB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9C;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAgB,EAChB,gBAAoB,EACpB,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,qBAAqB,EACrB,GAAG,KAAK,EACT,EAAE,qBAAqB,+BAsHvB"}
package/build/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo, useRef, useState } from 'react';
2
2
  import { PixelRatio, Platform, Pressable, StyleSheet, Text, View, } from 'react-native';
3
- import Animated, { runOnJS, useAnimatedScrollHandler, useDerivedValue, useSharedValue, } from 'react-native-reanimated';
3
+ import Animated, { runOnJS, useAnimatedReaction, useAnimatedScrollHandler, useSharedValue, } from 'react-native-reanimated';
4
4
  export function HorizontalPicker({ data, initialIndex = 0, visibleItemCount = 7, onChange, onHapticFeedback, containerStyle, itemContainerStyle, itemTextStyle, selectedItemTextStyle, ...props }) {
5
5
  const scrollViewRef = useRef(null);
6
6
  const lastHapticIndexRef = useRef(-1);
@@ -24,9 +24,9 @@ export function HorizontalPicker({ data, initialIndex = 0, visibleItemCount = 7,
24
24
  const safeIndex = Math.max(0, Math.min(data.length - 1, initialIndex));
25
25
  const rawItemWidth = layoutWidth / visibleItemCount;
26
26
  const x = PixelRatio.roundToNearestPixel(safeIndex * rawItemWidth);
27
- setTimeout(() => {
28
- scrollViewRef.current?.scrollTo({ x, y: 0, animated: true });
29
- }, 0);
27
+ requestAnimationFrame(() => {
28
+ scrollViewRef.current?.scrollTo({ x, y: 0, animated: false });
29
+ });
30
30
  }, [initialIndex, data.length, visibleItemCount]);
31
31
  const handleOnScroll = useAnimatedScrollHandler({
32
32
  onScroll: (event) => {
@@ -50,30 +50,31 @@ export function HorizontalPicker({ data, initialIndex = 0, visibleItemCount = 7,
50
50
  onHapticFeedback?.();
51
51
  }
52
52
  }, [data.length, onHapticFeedback]);
53
- useDerivedValue(() => {
53
+ useAnimatedReaction(() => {
54
54
  if (scrollViewWidth === 0) {
55
- return;
55
+ return null;
56
56
  }
57
57
  const newIndex = Math.round(scrollX.value / itemWidth);
58
58
  const safeIndex = Math.max(0, Math.min(data.length - 1, newIndex));
59
- if (safeIndex !== currentIndex.value) {
60
- currentIndex.value = safeIndex;
59
+ currentIndex.value = safeIndex;
60
+ return safeIndex;
61
+ }, (safeIndex, prevIndex) => {
62
+ if (safeIndex !== null && safeIndex !== prevIndex) {
61
63
  runOnJS(handleOnChange)(safeIndex);
62
64
  runOnJS(handleOnHapticFeedback)(safeIndex);
63
65
  }
64
- }, [itemWidth, data.length]);
66
+ }, [itemWidth, data.length, scrollViewWidth]);
65
67
  return (<Animated.ScrollView ref={scrollViewRef} horizontal onLayout={handleOnLayout} onScroll={handleOnScroll} showsHorizontalScrollIndicator={false} scrollEventThrottle={Platform.select({ ios: 16, android: 2 })} decelerationRate={Platform.select({ ios: undefined, android: 'fast' })} snapToOffsets={snapOffsets} contentContainerStyle={{ paddingHorizontal: paddingSide }} style={[styles.container, containerStyle]} {...props}>
66
68
  {data.map((item, index) => (<PickerItem key={`picker-item-${item.value}`} label={item.label} index={index} currentIndex={currentIndex} itemWidth={itemWidth} onPress={() => handleOnPress(index)} itemContainerStyle={itemContainerStyle} itemTextStyle={itemTextStyle} selectedItemTextStyle={selectedItemTextStyle}/>))}
67
69
  </Animated.ScrollView>);
68
70
  }
69
71
  function PickerItem({ label, index, currentIndex, itemWidth, onPress, itemContainerStyle, itemTextStyle, selectedItemTextStyle, }) {
70
72
  const [isFocused, setIsFocused] = useState(false);
71
- useDerivedValue(() => {
72
- const shouldBeFocused = currentIndex.value === index;
73
- if (shouldBeFocused !== isFocused) {
73
+ useAnimatedReaction(() => currentIndex.value === index, (shouldBeFocused, isCurrentFocused) => {
74
+ if (shouldBeFocused !== isCurrentFocused) {
74
75
  runOnJS(setIsFocused)(shouldBeFocused);
75
76
  }
76
- });
77
+ }, [index]);
77
78
  return (<Pressable onPress={onPress}>
78
79
  <View style={[{ width: itemWidth }, styles.itemContainer, itemContainerStyle]}>
79
80
  <Text style={isFocused ? [styles.itemTextSelected, selectedItemTextStyle] : [styles.itemText, itemTextStyle]}>
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react';\nimport {\n type LayoutChangeEvent,\n PixelRatio,\n Platform,\n Pressable,\n type StyleProp,\n StyleSheet,\n Text,\n type TextStyle,\n View,\n type ViewStyle,\n} from 'react-native';\nimport Animated, {\n type AnimatedScrollViewProps,\n runOnJS,\n type SharedValue,\n useAnimatedScrollHandler,\n useDerivedValue,\n useSharedValue,\n} from 'react-native-reanimated';\n\ninterface PickerOption {\n label: string;\n value: string | number;\n}\n\ninterface Props extends Omit<AnimatedScrollViewProps, 'style'> {\n data: PickerOption[];\n initialIndex?: number;\n visibleItemCount?: number;\n onChange?: (value: PickerOption['value'], index: number) => void;\n onHapticFeedback?: () => void;\n containerStyle?: AnimatedScrollViewProps['style'];\n itemContainerStyle?: StyleProp<ViewStyle>;\n itemTextStyle?: StyleProp<TextStyle>;\n selectedItemTextStyle?: StyleProp<TextStyle>;\n}\n\nexport function HorizontalPicker({\n data,\n initialIndex = 0,\n visibleItemCount = 7,\n onChange,\n onHapticFeedback,\n containerStyle,\n itemContainerStyle,\n itemTextStyle,\n selectedItemTextStyle,\n ...props\n}: Props) {\n const scrollViewRef = useRef<Animated.ScrollView>(null);\n const lastHapticIndexRef = useRef<number>(-1);\n const [scrollViewWidth, setScrollViewWidth] = useState<number>(0);\n\n const scrollX = useSharedValue<number>(0);\n const currentIndex = useSharedValue<number>(initialIndex);\n\n const { itemWidth, paddingSide } = useMemo(() => {\n const width = PixelRatio.roundToNearestPixel(scrollViewWidth / visibleItemCount);\n const padding = PixelRatio.roundToNearestPixel(scrollViewWidth / 2 - width / 2);\n return {\n itemWidth: width,\n paddingSide: padding,\n };\n }, [scrollViewWidth, visibleItemCount]);\n\n const snapOffsets = useMemo(() => {\n return data.map((_, index) => PixelRatio.roundToNearestPixel(index * itemWidth));\n }, [data, itemWidth]);\n\n const handleOnLayout = useCallback(\n (e: LayoutChangeEvent) => {\n const layoutWidth = e.nativeEvent.layout.width;\n setScrollViewWidth(layoutWidth);\n\n const safeIndex = Math.max(0, Math.min(data.length - 1, initialIndex));\n const rawItemWidth = layoutWidth / visibleItemCount;\n const x = PixelRatio.roundToNearestPixel(safeIndex * rawItemWidth);\n\n setTimeout(() => {\n scrollViewRef.current?.scrollTo({ x, y: 0, animated: true });\n }, 0);\n },\n [initialIndex, data.length, visibleItemCount],\n );\n\n const handleOnScroll = useAnimatedScrollHandler({\n onScroll: (event) => {\n scrollX.value = event.contentOffset.x;\n },\n });\n\n const handleOnPress = useCallback(\n (newIndex: number) => {\n const safeIndex = Math.max(0, Math.min(data.length - 1, newIndex));\n const x = PixelRatio.roundToNearestPixel(safeIndex * itemWidth);\n scrollViewRef.current?.scrollTo({ x, y: 0, animated: true });\n },\n [data.length, itemWidth],\n );\n\n const handleOnChange = useCallback(\n (index: number) => {\n const item = data[index];\n if (item) {\n onChange?.(item.value, index);\n }\n },\n [onChange, data],\n );\n\n const handleOnHapticFeedback = useCallback(\n (index: number) => {\n if (index !== lastHapticIndexRef.current && index >= 0 && index < data.length) {\n lastHapticIndexRef.current = index;\n onHapticFeedback?.();\n }\n },\n [data.length, onHapticFeedback],\n );\n\n useDerivedValue(() => {\n if (scrollViewWidth === 0) {\n return;\n }\n\n const newIndex = Math.round(scrollX.value / itemWidth);\n const safeIndex = Math.max(0, Math.min(data.length - 1, newIndex));\n\n if (safeIndex !== currentIndex.value) {\n currentIndex.value = safeIndex;\n runOnJS(handleOnChange)(safeIndex);\n runOnJS(handleOnHapticFeedback)(safeIndex);\n }\n }, [itemWidth, data.length]);\n\n return (\n <Animated.ScrollView\n ref={scrollViewRef}\n horizontal\n onLayout={handleOnLayout}\n onScroll={handleOnScroll}\n showsHorizontalScrollIndicator={false}\n scrollEventThrottle={Platform.select({ ios: 16, android: 2 })}\n decelerationRate={Platform.select({ ios: undefined, android: 'fast' })}\n snapToOffsets={snapOffsets}\n contentContainerStyle={{ paddingHorizontal: paddingSide }}\n style={[styles.container, containerStyle]}\n {...props}\n >\n {data.map((item, index) => (\n <PickerItem\n key={`picker-item-${item.value}`}\n label={item.label}\n index={index}\n currentIndex={currentIndex}\n itemWidth={itemWidth}\n onPress={() => handleOnPress(index)}\n itemContainerStyle={itemContainerStyle}\n itemTextStyle={itemTextStyle}\n selectedItemTextStyle={selectedItemTextStyle}\n />\n ))}\n </Animated.ScrollView>\n );\n}\n\nfunction PickerItem({\n label,\n index,\n currentIndex,\n itemWidth,\n onPress,\n itemContainerStyle,\n itemTextStyle,\n selectedItemTextStyle,\n}: {\n label: PickerOption['label'];\n index: number;\n currentIndex: SharedValue<number>;\n itemWidth: number;\n onPress: () => void;\n itemContainerStyle?: StyleProp<ViewStyle>;\n itemTextStyle?: StyleProp<TextStyle>;\n selectedItemTextStyle?: StyleProp<TextStyle>;\n}) {\n const [isFocused, setIsFocused] = useState(false);\n\n useDerivedValue(() => {\n const shouldBeFocused = currentIndex.value === index;\n if (shouldBeFocused !== isFocused) {\n runOnJS(setIsFocused)(shouldBeFocused);\n }\n });\n\n return (\n <Pressable onPress={onPress}>\n <View style={[{ width: itemWidth }, styles.itemContainer, itemContainerStyle]}>\n <Text style={isFocused ? [styles.itemTextSelected, selectedItemTextStyle] : [styles.itemText, itemTextStyle]}>\n {label}\n </Text>\n </View>\n </Pressable>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n width: '100%',\n },\n itemContainer: {\n justifyContent: 'center',\n alignItems: 'center',\n height: 60,\n },\n itemText: {\n fontSize: 13,\n fontWeight: '700',\n color: '#C9CED9',\n },\n itemTextSelected: {\n fontSize: 15,\n fontWeight: '800',\n color: '#000000',\n },\n});\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC/D,OAAO,EAEL,UAAU,EACV,QAAQ,EACR,SAAS,EAET,UAAU,EACV,IAAI,EAEJ,IAAI,GAEL,MAAM,cAAc,CAAC;AACtB,OAAO,QAAQ,EAAE,EAEf,OAAO,EAEP,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,GACf,MAAM,yBAAyB,CAAC;AAmBjC,MAAM,UAAU,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,GAAG,CAAC,EAChB,gBAAgB,GAAG,CAAC,EACpB,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,qBAAqB,EACrB,GAAG,KAAK,EACc;IACtB,MAAM,aAAa,GAAG,MAAM,CAAsB,IAAI,CAAC,CAAC;IACxD,MAAM,kBAAkB,GAAG,MAAM,CAAS,CAAC,CAAC,CAAC,CAAC;IAC9C,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAS,CAAC,CAAC,CAAC;IAClE,MAAM,OAAO,GAAG,cAAc,CAAS,CAAC,CAAC,CAAC;IAC1C,MAAM,YAAY,GAAG,cAAc,CAAS,YAAY,CAAC,CAAC;IAE1D,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE;QAC9C,MAAM,KAAK,GAAG,UAAU,CAAC,mBAAmB,CAAC,eAAe,GAAG,gBAAgB,CAAC,CAAC;QACjF,MAAM,OAAO,GAAG,UAAU,CAAC,mBAAmB,CAAC,eAAe,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC;QAChF,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,WAAW,EAAE,OAAO;SACrB,CAAC;IACJ,CAAC,EAAE,CAAC,eAAe,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAExC,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE;QAC/B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,mBAAmB,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC;IACnF,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;IAEtB,MAAM,cAAc,GAAG,WAAW,CAChC,CAAC,CAAoB,EAAE,EAAE;QACvB,MAAM,WAAW,GAAG,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC;QAC/C,kBAAkB,CAAC,WAAW,CAAC,CAAC;QAEhC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC;QACvE,MAAM,YAAY,GAAG,WAAW,GAAG,gBAAgB,CAAC;QACpD,MAAM,CAAC,GAAG,UAAU,CAAC,mBAAmB,CAAC,SAAS,GAAG,YAAY,CAAC,CAAC;QACnE,qBAAqB,CAAC,GAAG,EAAE;YACzB,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;IACL,CAAC,EACD,CAAC,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAC9C,CAAC;IAEF,MAAM,cAAc,GAAG,wBAAwB,CAAC;QAC9C,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;YAClB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC;QACxC,CAAC;KACF,CAAC,CAAC;IAEH,MAAM,aAAa,GAAG,WAAW,CAC/B,CAAC,QAAgB,EAAE,EAAE;QACnB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QACnE,MAAM,CAAC,GAAG,UAAU,CAAC,mBAAmB,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;QAChE,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC,EACD,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CACzB,CAAC;IAEF,MAAM,cAAc,GAAG,WAAW,CAChC,CAAC,KAAa,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,IAAI,EAAE,CAAC;YACT,QAAQ,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAChC,CAAC;IACH,CAAC,EACD,CAAC,QAAQ,EAAE,IAAI,CAAC,CACjB,CAAC;IAEF,MAAM,sBAAsB,GAAG,WAAW,CACxC,CAAC,KAAa,EAAE,EAAE;QAChB,IAAI,KAAK,KAAK,kBAAkB,CAAC,OAAO,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAC9E,kBAAkB,CAAC,OAAO,GAAG,KAAK,CAAC;YACnC,gBAAgB,EAAE,EAAE,CAAC;QACvB,CAAC;IACH,CAAC,EACD,CAAC,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAChC,CAAC;IAEF,mBAAmB,CACjB,GAAG,EAAE;QACH,IAAI,eAAe,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;QACvD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QACnE,YAAY,CAAC,KAAK,GAAG,SAAS,CAAC;QAC/B,OAAO,SAAS,CAAC;IACnB,CAAC,EACD,CAAC,SAAS,EAAE,SAAS,EAAE,EAAE;QACvB,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAClD,OAAO,CAAC,cAAc,CAAC,CAAC,SAAS,CAAC,CAAC;YACnC,OAAO,CAAC,sBAAsB,CAAC,CAAC,SAAS,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,EACD,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAC1C,CAAC;IAEF,OAAO,CACL,CAAC,QAAQ,CAAC,UAAU,CAClB,GAAG,CAAC,CAAC,aAAa,CAAC,CACnB,UAAU,CACV,QAAQ,CAAC,CAAC,cAAc,CAAC,CACzB,QAAQ,CAAC,CAAC,cAAc,CAAC,CACzB,8BAA8B,CAAC,CAAC,KAAK,CAAC,CACtC,mBAAmB,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAC9D,gBAAgB,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CACvE,aAAa,CAAC,CAAC,WAAW,CAAC,CAC3B,qBAAqB,CAAC,CAAC,EAAE,iBAAiB,EAAE,WAAW,EAAE,CAAC,CAC1D,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC,CAC1C,IAAI,KAAK,CAAC,CAEV;MAAA,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CACzB,CAAC,UAAU,CACT,GAAG,CAAC,CAAC,eAAe,IAAI,CAAC,KAAK,EAAE,CAAC,CACjC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAClB,KAAK,CAAC,CAAC,KAAK,CAAC,CACb,YAAY,CAAC,CAAC,YAAY,CAAC,CAC3B,SAAS,CAAC,CAAC,SAAS,CAAC,CACrB,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CACpC,kBAAkB,CAAC,CAAC,kBAAkB,CAAC,CACvC,aAAa,CAAC,CAAC,aAAa,CAAC,CAC7B,qBAAqB,CAAC,CAAC,qBAAqB,CAAC,EAC7C,CACH,CAAC,CACJ;IAAA,EAAE,QAAQ,CAAC,UAAU,CAAC,CACvB,CAAC;AACJ,CAAC;AAWD,SAAS,UAAU,CAAC,EAClB,KAAK,EACL,KAAK,EACL,YAAY,EACZ,SAAS,EACT,OAAO,EACP,kBAAkB,EAClB,aAAa,EACb,qBAAqB,GACL;IAChB,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAElD,mBAAmB,CACjB,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,KAAK,KAAK,EAClC,CAAC,eAAe,EAAE,gBAAgB,EAAE,EAAE;QACpC,IAAI,eAAe,KAAK,gBAAgB,EAAE,CAAC;YACzC,OAAO,CAAC,YAAY,CAAC,CAAC,eAAe,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,EACD,CAAC,KAAK,CAAC,CACR,CAAC;IAEF,OAAO,CACL,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAC1B;MAAA,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,aAAa,EAAE,kBAAkB,CAAC,CAAC,CAC5E;QAAA,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAC3G;UAAA,CAAC,KAAK,CACR;QAAA,EAAE,IAAI,CACR;MAAA,EAAE,IAAI,CACR;IAAA,EAAE,SAAS,CAAC,CACb,CAAC;AACJ,CAAC;AAED,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAC/B,SAAS,EAAE;QACT,KAAK,EAAE,MAAM;KACd;IACD,aAAa,EAAE;QACb,cAAc,EAAE,QAAQ;QACxB,UAAU,EAAE,QAAQ;QACpB,MAAM,EAAE,EAAE;KACX;IACD,QAAQ,EAAE;QACR,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,KAAK;QACjB,KAAK,EAAE,SAAS;KACjB;IACD,gBAAgB,EAAE;QAChB,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,KAAK;QACjB,KAAK,EAAE,SAAS;KACjB;CACF,CAAC,CAAC","sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react';\nimport {\n type LayoutChangeEvent,\n PixelRatio,\n Platform,\n Pressable,\n type StyleProp,\n StyleSheet,\n Text,\n type TextStyle,\n View,\n type ViewStyle,\n} from 'react-native';\nimport Animated, {\n type AnimatedScrollViewProps,\n runOnJS,\n type SharedValue,\n useAnimatedReaction,\n useAnimatedScrollHandler,\n useSharedValue,\n} from 'react-native-reanimated';\n\ninterface PickerOption {\n label: string;\n value: string | number;\n}\n\ninterface HorizontalPickerProps extends Omit<AnimatedScrollViewProps, 'style'> {\n data: PickerOption[];\n initialIndex?: number;\n visibleItemCount?: number;\n onChange?: (value: PickerOption['value'], index: number) => void;\n onHapticFeedback?: () => void;\n containerStyle?: AnimatedScrollViewProps['style'];\n itemContainerStyle?: StyleProp<ViewStyle>;\n itemTextStyle?: StyleProp<TextStyle>;\n selectedItemTextStyle?: StyleProp<TextStyle>;\n}\n\nexport function HorizontalPicker({\n data,\n initialIndex = 0,\n visibleItemCount = 7,\n onChange,\n onHapticFeedback,\n containerStyle,\n itemContainerStyle,\n itemTextStyle,\n selectedItemTextStyle,\n ...props\n}: HorizontalPickerProps) {\n const scrollViewRef = useRef<Animated.ScrollView>(null);\n const lastHapticIndexRef = useRef<number>(-1);\n const [scrollViewWidth, setScrollViewWidth] = useState<number>(0);\n const scrollX = useSharedValue<number>(0);\n const currentIndex = useSharedValue<number>(initialIndex);\n\n const { itemWidth, paddingSide } = useMemo(() => {\n const width = PixelRatio.roundToNearestPixel(scrollViewWidth / visibleItemCount);\n const padding = PixelRatio.roundToNearestPixel(scrollViewWidth / 2 - width / 2);\n return {\n itemWidth: width,\n paddingSide: padding,\n };\n }, [scrollViewWidth, visibleItemCount]);\n\n const snapOffsets = useMemo(() => {\n return data.map((_, index) => PixelRatio.roundToNearestPixel(index * itemWidth));\n }, [data, itemWidth]);\n\n const handleOnLayout = useCallback(\n (e: LayoutChangeEvent) => {\n const layoutWidth = e.nativeEvent.layout.width;\n setScrollViewWidth(layoutWidth);\n\n const safeIndex = Math.max(0, Math.min(data.length - 1, initialIndex));\n const rawItemWidth = layoutWidth / visibleItemCount;\n const x = PixelRatio.roundToNearestPixel(safeIndex * rawItemWidth);\n requestAnimationFrame(() => {\n scrollViewRef.current?.scrollTo({ x, y: 0, animated: false });\n });\n },\n [initialIndex, data.length, visibleItemCount],\n );\n\n const handleOnScroll = useAnimatedScrollHandler({\n onScroll: (event) => {\n scrollX.value = event.contentOffset.x;\n },\n });\n\n const handleOnPress = useCallback(\n (newIndex: number) => {\n const safeIndex = Math.max(0, Math.min(data.length - 1, newIndex));\n const x = PixelRatio.roundToNearestPixel(safeIndex * itemWidth);\n scrollViewRef.current?.scrollTo({ x, y: 0, animated: true });\n },\n [data.length, itemWidth],\n );\n\n const handleOnChange = useCallback(\n (index: number) => {\n const item = data[index];\n if (item) {\n onChange?.(item.value, index);\n }\n },\n [onChange, data],\n );\n\n const handleOnHapticFeedback = useCallback(\n (index: number) => {\n if (index !== lastHapticIndexRef.current && index >= 0 && index < data.length) {\n lastHapticIndexRef.current = index;\n onHapticFeedback?.();\n }\n },\n [data.length, onHapticFeedback],\n );\n\n useAnimatedReaction(\n () => {\n if (scrollViewWidth === 0) {\n return null;\n }\n const newIndex = Math.round(scrollX.value / itemWidth);\n const safeIndex = Math.max(0, Math.min(data.length - 1, newIndex));\n currentIndex.value = safeIndex;\n return safeIndex;\n },\n (safeIndex, prevIndex) => {\n if (safeIndex !== null && safeIndex !== prevIndex) {\n runOnJS(handleOnChange)(safeIndex);\n runOnJS(handleOnHapticFeedback)(safeIndex);\n }\n },\n [itemWidth, data.length, scrollViewWidth],\n );\n\n return (\n <Animated.ScrollView\n ref={scrollViewRef}\n horizontal\n onLayout={handleOnLayout}\n onScroll={handleOnScroll}\n showsHorizontalScrollIndicator={false}\n scrollEventThrottle={Platform.select({ ios: 16, android: 2 })}\n decelerationRate={Platform.select({ ios: undefined, android: 'fast' })}\n snapToOffsets={snapOffsets}\n contentContainerStyle={{ paddingHorizontal: paddingSide }}\n style={[styles.container, containerStyle]}\n {...props}\n >\n {data.map((item, index) => (\n <PickerItem\n key={`picker-item-${item.value}`}\n label={item.label}\n index={index}\n currentIndex={currentIndex}\n itemWidth={itemWidth}\n onPress={() => handleOnPress(index)}\n itemContainerStyle={itemContainerStyle}\n itemTextStyle={itemTextStyle}\n selectedItemTextStyle={selectedItemTextStyle}\n />\n ))}\n </Animated.ScrollView>\n );\n}\n\ninterface PickerItemProps\n extends Pick<HorizontalPickerProps, 'itemContainerStyle' | 'itemTextStyle' | 'selectedItemTextStyle'> {\n label: PickerOption['label'];\n index: number;\n currentIndex: SharedValue<number>;\n itemWidth: number;\n onPress: () => void;\n}\n\nfunction PickerItem({\n label,\n index,\n currentIndex,\n itemWidth,\n onPress,\n itemContainerStyle,\n itemTextStyle,\n selectedItemTextStyle,\n}: PickerItemProps) {\n const [isFocused, setIsFocused] = useState(false);\n\n useAnimatedReaction(\n () => currentIndex.value === index,\n (shouldBeFocused, isCurrentFocused) => {\n if (shouldBeFocused !== isCurrentFocused) {\n runOnJS(setIsFocused)(shouldBeFocused);\n }\n },\n [index],\n );\n\n return (\n <Pressable onPress={onPress}>\n <View style={[{ width: itemWidth }, styles.itemContainer, itemContainerStyle]}>\n <Text style={isFocused ? [styles.itemTextSelected, selectedItemTextStyle] : [styles.itemText, itemTextStyle]}>\n {label}\n </Text>\n </View>\n </Pressable>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n width: '100%',\n },\n itemContainer: {\n justifyContent: 'center',\n alignItems: 'center',\n height: 60,\n },\n itemText: {\n fontSize: 13,\n fontWeight: '700',\n color: '#C9CED9',\n },\n itemTextSelected: {\n fontSize: 15,\n fontWeight: '800',\n color: '#000000',\n },\n});\n"]}
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "expo-horizontal-picker",
3
- "version": "0.1.0",
4
- "description": "expo-horizontal-picker",
3
+ "version": "0.1.2",
4
+ "description": "A performant horizontal picker component for React Native and Expo apps",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "expo-module build",
9
9
  "changeset": "changeset",
10
10
  "changeset:publish": "yarn build && changeset publish",
11
- "changeset:version": "changeset version && pnpm i --lockfile-only",
11
+ "changeset:version": "changeset version && yarn --lockfile-only",
12
12
  "clean": "expo-module clean",
13
13
  "lint": "expo-module lint",
14
14
  "test": "expo-module test",
@@ -49,6 +49,9 @@
49
49
  "typescript": "^5.8.3",
50
50
  "react-native-reanimated": "^3.18.0"
51
51
  },
52
+ "files": [
53
+ "/build"
54
+ ],
52
55
  "peerDependencies": {
53
56
  "expo": "*",
54
57
  "react": "*",
package/.lintstagedrc DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "*.{js,ts,jsx,tsx}": ["biome format", "biome lint"]
3
- }
package/.nvmrc DELETED
@@ -1 +0,0 @@
1
- v22.11.0
package/biome.json DELETED
@@ -1,77 +0,0 @@
1
- {
2
- "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3
- "formatter": {
4
- "indentWidth": 2,
5
- "lineWidth": 120,
6
- "indentStyle": "space",
7
- "enabled": true,
8
- "bracketSpacing": true,
9
- "formatWithErrors": false
10
- },
11
- "javascript": {
12
- "formatter": {
13
- "quoteStyle": "single",
14
- "trailingCommas": "all",
15
- "semicolons": "always",
16
- "bracketSameLine": false
17
- }
18
- },
19
- "linter": {
20
- "enabled": true,
21
- "rules": {
22
- "recommended": true,
23
- "a11y": {
24
- "useButtonType": "off"
25
- },
26
- "complexity": {
27
- "noVoid": "error",
28
- "noBannedTypes": "error",
29
- "noExtraBooleanCast": "error",
30
- "noExcessiveCognitiveComplexity": "error"
31
- },
32
- "correctness": {
33
- "noUnusedImports": "error",
34
- "noUnusedVariables": "error",
35
- "useHookAtTopLevel": "error"
36
- },
37
- "performance": {
38
- "noBarrelFile": "error",
39
- "noReExportAll": "error",
40
- "noDelete": "error"
41
- },
42
- "security": {
43
- "noDangerouslySetInnerHtml": "error"
44
- },
45
- "style": {
46
- "noUselessElse": "error",
47
- "useCollapsedElseIf": "error",
48
- "noNonNullAssertion": "error",
49
- "useBlockStatements": "error",
50
- "noNamespace": "error",
51
- "noYodaExpression": "error",
52
- "useConsistentBuiltinInstantiation": "error",
53
- "useDefaultSwitchClause": "error",
54
- "useFragmentSyntax": "error",
55
- "useThrowNewError": "error",
56
- "useThrowOnlyError": "error",
57
- "useFilenamingConvention": {
58
- "level": "error",
59
- "options": {
60
- "filenameCases": ["camelCase", "PascalCase"]
61
- }
62
- }
63
- },
64
- "suspicious": {
65
- "noArrayIndexKey": "error",
66
- "noAssignInExpressions": "error",
67
- "noConfusingVoidType": "error",
68
- "noConsole": "error",
69
- "noDebugger": "error",
70
- "noExplicitAny": "error",
71
- "noRedeclare": "error",
72
- "noEvolvingTypes": "error",
73
- "useNumberToFixedDigitsArgument": "error"
74
- }
75
- }
76
- }
77
- }
@@ -1 +0,0 @@
1
- module.exports = { extends: ['@commitlint/config-conventional'] };
@@ -1,9 +0,0 @@
1
- {
2
- "platforms": ["apple", "android", "web"],
3
- "apple": {
4
- "modules": ["ExpoHorizontalPickerModule"]
5
- },
6
- "android": {
7
- "modules": ["expo.modules.horizontalpicker.ExpoHorizontalPickerModule"]
8
- }
9
- }
package/knip.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/knip@5/schema.json",
3
- "workspaces": {
4
- ".": {
5
- "ignore": ["packlint.config.mjs"]
6
- }
7
- }
8
- }
@@ -1,3 +0,0 @@
1
- export default {
2
- files: ['./package.json'],
3
- };
package/src/index.tsx DELETED
@@ -1,227 +0,0 @@
1
- import { useCallback, useMemo, useRef, useState } from 'react';
2
- import {
3
- type LayoutChangeEvent,
4
- PixelRatio,
5
- Platform,
6
- Pressable,
7
- type StyleProp,
8
- StyleSheet,
9
- Text,
10
- type TextStyle,
11
- View,
12
- type ViewStyle,
13
- } from 'react-native';
14
- import Animated, {
15
- type AnimatedScrollViewProps,
16
- runOnJS,
17
- type SharedValue,
18
- useAnimatedScrollHandler,
19
- useDerivedValue,
20
- useSharedValue,
21
- } from 'react-native-reanimated';
22
-
23
- interface PickerOption {
24
- label: string;
25
- value: string | number;
26
- }
27
-
28
- interface Props extends Omit<AnimatedScrollViewProps, 'style'> {
29
- data: PickerOption[];
30
- initialIndex?: number;
31
- visibleItemCount?: number;
32
- onChange?: (value: PickerOption['value'], index: number) => void;
33
- onHapticFeedback?: () => void;
34
- containerStyle?: AnimatedScrollViewProps['style'];
35
- itemContainerStyle?: StyleProp<ViewStyle>;
36
- itemTextStyle?: StyleProp<TextStyle>;
37
- selectedItemTextStyle?: StyleProp<TextStyle>;
38
- }
39
-
40
- export function HorizontalPicker({
41
- data,
42
- initialIndex = 0,
43
- visibleItemCount = 7,
44
- onChange,
45
- onHapticFeedback,
46
- containerStyle,
47
- itemContainerStyle,
48
- itemTextStyle,
49
- selectedItemTextStyle,
50
- ...props
51
- }: Props) {
52
- const scrollViewRef = useRef<Animated.ScrollView>(null);
53
- const lastHapticIndexRef = useRef<number>(-1);
54
- const [scrollViewWidth, setScrollViewWidth] = useState<number>(0);
55
-
56
- const scrollX = useSharedValue<number>(0);
57
- const currentIndex = useSharedValue<number>(initialIndex);
58
-
59
- const { itemWidth, paddingSide } = useMemo(() => {
60
- const width = PixelRatio.roundToNearestPixel(scrollViewWidth / visibleItemCount);
61
- const padding = PixelRatio.roundToNearestPixel(scrollViewWidth / 2 - width / 2);
62
- return {
63
- itemWidth: width,
64
- paddingSide: padding,
65
- };
66
- }, [scrollViewWidth, visibleItemCount]);
67
-
68
- const snapOffsets = useMemo(() => {
69
- return data.map((_, index) => PixelRatio.roundToNearestPixel(index * itemWidth));
70
- }, [data, itemWidth]);
71
-
72
- const handleOnLayout = useCallback(
73
- (e: LayoutChangeEvent) => {
74
- const layoutWidth = e.nativeEvent.layout.width;
75
- setScrollViewWidth(layoutWidth);
76
-
77
- const safeIndex = Math.max(0, Math.min(data.length - 1, initialIndex));
78
- const rawItemWidth = layoutWidth / visibleItemCount;
79
- const x = PixelRatio.roundToNearestPixel(safeIndex * rawItemWidth);
80
-
81
- setTimeout(() => {
82
- scrollViewRef.current?.scrollTo({ x, y: 0, animated: true });
83
- }, 0);
84
- },
85
- [initialIndex, data.length, visibleItemCount],
86
- );
87
-
88
- const handleOnScroll = useAnimatedScrollHandler({
89
- onScroll: (event) => {
90
- scrollX.value = event.contentOffset.x;
91
- },
92
- });
93
-
94
- const handleOnPress = useCallback(
95
- (newIndex: number) => {
96
- const safeIndex = Math.max(0, Math.min(data.length - 1, newIndex));
97
- const x = PixelRatio.roundToNearestPixel(safeIndex * itemWidth);
98
- scrollViewRef.current?.scrollTo({ x, y: 0, animated: true });
99
- },
100
- [data.length, itemWidth],
101
- );
102
-
103
- const handleOnChange = useCallback(
104
- (index: number) => {
105
- const item = data[index];
106
- if (item) {
107
- onChange?.(item.value, index);
108
- }
109
- },
110
- [onChange, data],
111
- );
112
-
113
- const handleOnHapticFeedback = useCallback(
114
- (index: number) => {
115
- if (index !== lastHapticIndexRef.current && index >= 0 && index < data.length) {
116
- lastHapticIndexRef.current = index;
117
- onHapticFeedback?.();
118
- }
119
- },
120
- [data.length, onHapticFeedback],
121
- );
122
-
123
- useDerivedValue(() => {
124
- if (scrollViewWidth === 0) {
125
- return;
126
- }
127
-
128
- const newIndex = Math.round(scrollX.value / itemWidth);
129
- const safeIndex = Math.max(0, Math.min(data.length - 1, newIndex));
130
-
131
- if (safeIndex !== currentIndex.value) {
132
- currentIndex.value = safeIndex;
133
- runOnJS(handleOnChange)(safeIndex);
134
- runOnJS(handleOnHapticFeedback)(safeIndex);
135
- }
136
- }, [itemWidth, data.length]);
137
-
138
- return (
139
- <Animated.ScrollView
140
- ref={scrollViewRef}
141
- horizontal
142
- onLayout={handleOnLayout}
143
- onScroll={handleOnScroll}
144
- showsHorizontalScrollIndicator={false}
145
- scrollEventThrottle={Platform.select({ ios: 16, android: 2 })}
146
- decelerationRate={Platform.select({ ios: undefined, android: 'fast' })}
147
- snapToOffsets={snapOffsets}
148
- contentContainerStyle={{ paddingHorizontal: paddingSide }}
149
- style={[styles.container, containerStyle]}
150
- {...props}
151
- >
152
- {data.map((item, index) => (
153
- <PickerItem
154
- key={`picker-item-${item.value}`}
155
- label={item.label}
156
- index={index}
157
- currentIndex={currentIndex}
158
- itemWidth={itemWidth}
159
- onPress={() => handleOnPress(index)}
160
- itemContainerStyle={itemContainerStyle}
161
- itemTextStyle={itemTextStyle}
162
- selectedItemTextStyle={selectedItemTextStyle}
163
- />
164
- ))}
165
- </Animated.ScrollView>
166
- );
167
- }
168
-
169
- function PickerItem({
170
- label,
171
- index,
172
- currentIndex,
173
- itemWidth,
174
- onPress,
175
- itemContainerStyle,
176
- itemTextStyle,
177
- selectedItemTextStyle,
178
- }: {
179
- label: PickerOption['label'];
180
- index: number;
181
- currentIndex: SharedValue<number>;
182
- itemWidth: number;
183
- onPress: () => void;
184
- itemContainerStyle?: StyleProp<ViewStyle>;
185
- itemTextStyle?: StyleProp<TextStyle>;
186
- selectedItemTextStyle?: StyleProp<TextStyle>;
187
- }) {
188
- const [isFocused, setIsFocused] = useState(false);
189
-
190
- useDerivedValue(() => {
191
- const shouldBeFocused = currentIndex.value === index;
192
- if (shouldBeFocused !== isFocused) {
193
- runOnJS(setIsFocused)(shouldBeFocused);
194
- }
195
- });
196
-
197
- return (
198
- <Pressable onPress={onPress}>
199
- <View style={[{ width: itemWidth }, styles.itemContainer, itemContainerStyle]}>
200
- <Text style={isFocused ? [styles.itemTextSelected, selectedItemTextStyle] : [styles.itemText, itemTextStyle]}>
201
- {label}
202
- </Text>
203
- </View>
204
- </Pressable>
205
- );
206
- }
207
-
208
- const styles = StyleSheet.create({
209
- container: {
210
- width: '100%',
211
- },
212
- itemContainer: {
213
- justifyContent: 'center',
214
- alignItems: 'center',
215
- height: 60,
216
- },
217
- itemText: {
218
- fontSize: 13,
219
- fontWeight: '700',
220
- color: '#C9CED9',
221
- },
222
- itemTextSelected: {
223
- fontSize: 15,
224
- fontWeight: '800',
225
- color: '#000000',
226
- },
227
- });
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- // @generated by expo-module-scripts
2
- {
3
- "extends": "expo-module-scripts/tsconfig.base",
4
- "compilerOptions": {
5
- "outDir": "./build"
6
- },
7
- "include": ["./src"],
8
- "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
9
- }