diff --git a/example/package.json b/example/package.json index 6a3f8a7..e057c98 100644 --- a/example/package.json +++ b/example/package.json @@ -18,6 +18,7 @@ "react-dom": "19.0.0", "react-native": "0.79.2", "react-native-reanimated": "~3.17.4", + "react-native-safe-area-context": "5.4.0", "react-native-web": "^0.20.0", "tailwind-merge": "^2.6.0", "tailwindcss": "^3.4.17" diff --git a/example/src/App.tsx b/example/src/App.tsx index a6b99d8..07ec8a9 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,11 +1,6 @@ -import { - View, - ScrollView, - SafeAreaView, - Text, - TouchableOpacity, -} from 'react-native'; -import { useState } from 'react'; +import { FlatList, Pressable, View, Text } from 'react-native'; +import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; +import { useCallback, useState } from 'react'; import StripeOTPInput from './examples/stripe'; import AppleOTPInput from './examples/apple'; import RevoltOTPInput from './examples/revolt'; @@ -20,144 +15,149 @@ import FocusSlotExample from './examples/focus-slot'; type TabType = 'regular' | 'nativewind'; +const EXAMPLES = [ + { + title: 'Stripe', + description: 'Clean, minimal design with subtle borders', + regular: StripeOTPInput, + nativewind: StripeNativewind, + color: '#6772E5', + }, + { + title: 'Animated Stripe', + description: 'Stripe style with slide-in animation', + regular: AnimatedStripeOTPInput, + nativewind: AnimatedStripeOTPInput, + color: '#8B5CF6', + }, + { + title: 'Apple', + description: 'iOS-style with rounded corners and shadows', + regular: AppleOTPInput, + nativewind: AppleNativewind, + color: '#007AFF', + }, + { + title: 'Revolt', + description: 'Modern design with gradient backgrounds', + regular: RevoltOTPInput, + nativewind: RevoltNativewind, + color: '#FF6B6B', + }, + { + title: 'Dashed', + description: 'Simple dashed border style', + regular: DashedOTPInput, + nativewind: DashedNativewind, + color: '#4ECDC4', + }, + { + title: 'Animated Dashed', + description: 'Dashed style with slide-in animation', + regular: AnimatedDashedNativewind, + nativewind: AnimatedDashedNativewind, + color: '#8B5CF6', + }, + { + title: 'Focus Slot', + description: 'Tap any slot to focus it using slot.focus()', + regular: FocusSlotExample, + nativewind: FocusSlotExample, + color: '#10B981', + }, +]; + +type Example = (typeof EXAMPLES)[number]; + export default function App() { const [activeTab, setActiveTab] = useState('regular'); - const examples = [ - { - title: 'Stripe', - description: 'Clean, minimal design with subtle borders', - regular: StripeOTPInput, - nativewind: StripeNativewind, - color: '#6772E5', - }, - { - title: 'Animated Stripe', - description: 'Stripe style with slide-in animation', - regular: AnimatedStripeOTPInput, - nativewind: AnimatedStripeOTPInput, - color: '#8B5CF6', - }, - { - title: 'Apple', - description: 'iOS-style with rounded corners and shadows', - regular: AppleOTPInput, - nativewind: AppleNativewind, - color: '#007AFF', - }, - { - title: 'Revolt', - description: 'Modern design with gradient backgrounds', - regular: RevoltOTPInput, - nativewind: RevoltNativewind, - color: '#FF6B6B', - }, - { - title: 'Dashed', - description: 'Simple dashed border style', - regular: DashedOTPInput, - nativewind: DashedNativewind, - color: '#4ECDC4', - }, - { - title: 'Animated Dashed', - description: 'Dashed style with slide-in animation', - regular: AnimatedDashedNativewind, - nativewind: AnimatedDashedNativewind, - color: '#8B5CF6', - }, - { - title: 'Focus Slot', - description: 'Tap any slot to focus it using slot.focus()', - regular: FocusSlotExample, - nativewind: FocusSlotExample, - color: '#10B981', - }, - ]; + const renderExample = useCallback( + ({ item: example }: { item: Example }) => ( + + + + + {example.title} + + + {example.description} + + + - return ( - - - - OTP Input Examples - - - Beautiful, customizable one-time password inputs - + + {activeTab === 'regular' ? ( + + ) : ( + + )} + + ), + [activeTab] + ); - - setActiveTab('regular')} - > - - StyleSheet + return ( + + + + + OTP Input Examples - - setActiveTab('nativewind')} - > - - NativeWind + + Beautiful, customizable one-time password inputs - - + - - {examples.map((example) => ( - + setActiveTab('regular')} + style={({ pressed }) => ({ opacity: pressed ? 0.2 : 1 })} > - - {/* - - {example.title[0]} - - */} - - - {example.title} - - - {example.description} - - - + + StyleSheet + + + setActiveTab('nativewind')} + style={({ pressed }) => ({ opacity: pressed ? 0.2 : 1 })} + > + + NativeWind + + + - - {activeTab === 'regular' ? ( - - ) : ( - - )} + example.title} + renderItem={renderExample} + showsVerticalScrollIndicator={false} + ListFooterComponent={ + + + Built with ❤️ using input-otp-native + - - ))} - - - - Built with ❤️ using input-otp-native - - - - + } + /> + + ); } diff --git a/example/src/examples/animated-dashed-nativewind.tsx b/example/src/examples/animated-dashed-nativewind.tsx index 46fd67a..b342c7c 100644 --- a/example/src/examples/animated-dashed-nativewind.tsx +++ b/example/src/examples/animated-dashed-nativewind.tsx @@ -8,10 +8,9 @@ import Animated, { withRepeat, withTiming, withSequence, - useSharedValue, + useDerivedValue, FadeInDown, } from 'react-native-reanimated'; -import { useEffect } from 'react'; import { cn } from './utils'; export default function AnimatedDashedOTPInput() { @@ -29,8 +28,8 @@ export default function AnimatedDashedOTPInput() { maxLength={5} render={({ slots }) => ( - {slots.map((slot, idx) => ( - + {slots.map((slot, slotIndex) => ( + ))} )} @@ -67,18 +66,18 @@ function Slot({ char, isActive, hasFakeCaret, focus }: SlotProps) { } function FakeCaret() { - const opacity = useSharedValue(1); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/examples/animated-stripe-nativewind.tsx b/example/src/examples/animated-stripe-nativewind.tsx index 0bd3452..b2252be 100644 --- a/example/src/examples/animated-stripe-nativewind.tsx +++ b/example/src/examples/animated-stripe-nativewind.tsx @@ -9,6 +9,7 @@ import Animated, { withTiming, withSequence, useSharedValue, + useDerivedValue, FadeInDown, } from 'react-native-reanimated'; import { useEffect } from 'react'; @@ -29,14 +30,14 @@ export default function AnimatedStripeOTPInput() { render={({ slots }) => ( - {slots.slice(0, 3).map((slot, idx) => ( - + {slots.slice(0, 3).map((slot, slotIndex) => ( + ))} - {slots.slice(3).map((slot, idx) => ( - + {slots.slice(3).map((slot, slotIndex) => ( + ))} @@ -124,28 +125,30 @@ function FakeDash() { } function FakeCaret() { - const opacity = useSharedValue(1); - const height = useSharedValue(24); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - - height.value = withRepeat( - withSequence( - withTiming(20, { duration: 500 }), - withTiming(28, { duration: 500 }) + [] + ); + const height = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(20, { duration: 500 }), + withTiming(28, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity, height]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/examples/apple-nativewind.tsx b/example/src/examples/apple-nativewind.tsx index fca19e0..523c098 100644 --- a/example/src/examples/apple-nativewind.tsx +++ b/example/src/examples/apple-nativewind.tsx @@ -8,9 +8,8 @@ import Animated, { withRepeat, withTiming, withSequence, - useSharedValue, + useDerivedValue, } from 'react-native-reanimated'; -import { useEffect } from 'react'; import { cn } from './utils'; export default function AppleOTPInput() { @@ -27,8 +26,8 @@ export default function AppleOTPInput() { maxLength={5} render={({ slots }) => ( - {slots.map((slot, idx) => ( - + {slots.map((slot, slotIndex) => ( + ))} )} @@ -56,18 +55,18 @@ function Slot({ char, isActive, hasFakeCaret, focus }: SlotProps) { } function FakeCaret() { - const opacity = useSharedValue(1); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/examples/apple.tsx b/example/src/examples/apple.tsx index 0496a0d..181642a 100644 --- a/example/src/examples/apple.tsx +++ b/example/src/examples/apple.tsx @@ -15,9 +15,8 @@ import Animated, { withRepeat, withTiming, withSequence, - useSharedValue, + useDerivedValue, } from 'react-native-reanimated'; -import { useEffect } from 'react'; export default function AppleOTPInput() { const ref = useRef(null); @@ -35,8 +34,8 @@ export default function AppleOTPInput() { maxLength={5} render={({ slots }) => ( - {slots.map((slot, idx) => ( - + {slots.map((slot, slotIndex) => ( + ))} )} @@ -58,18 +57,18 @@ function Slot({ char, isActive, hasFakeCaret, focus }: SlotProps) { } function FakeCaret({ style }: { style?: ViewStyle }) { - const opacity = useSharedValue(1); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/examples/dashed-nativewind.tsx b/example/src/examples/dashed-nativewind.tsx index cd3b649..e521d73 100644 --- a/example/src/examples/dashed-nativewind.tsx +++ b/example/src/examples/dashed-nativewind.tsx @@ -8,9 +8,8 @@ import Animated, { withRepeat, withTiming, withSequence, - useSharedValue, + useDerivedValue, } from 'react-native-reanimated'; -import { useEffect } from 'react'; import { cn } from './utils'; export default function DashedOTPInput() { @@ -28,8 +27,8 @@ export default function DashedOTPInput() { maxLength={5} render={({ slots }) => ( - {slots.map((slot, idx) => ( - + {slots.map((slot, slotIndex) => ( + ))} )} @@ -58,18 +57,18 @@ function Slot({ char, isActive, hasFakeCaret, focus }: SlotProps) { } function FakeCaret() { - const opacity = useSharedValue(1); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/examples/dashed.tsx b/example/src/examples/dashed.tsx index 4ddd415..1ddc277 100644 --- a/example/src/examples/dashed.tsx +++ b/example/src/examples/dashed.tsx @@ -15,9 +15,8 @@ import Animated, { withRepeat, withTiming, withSequence, - useSharedValue, + useDerivedValue, } from 'react-native-reanimated'; -import { useEffect } from 'react'; export default function DashedOTPInput() { const ref = useRef(null); @@ -35,8 +34,8 @@ export default function DashedOTPInput() { maxLength={5} render={({ slots }) => ( - {slots.map((slot, idx) => ( - + {slots.map((slot, slotIndex) => ( + ))} )} @@ -56,18 +55,18 @@ function Slot({ char, isActive, hasFakeCaret, focus }: SlotProps) { } function FakeCaret({ style }: { style?: ViewStyle }) { - const opacity = useSharedValue(1); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/examples/focus-slot.tsx b/example/src/examples/focus-slot.tsx index 75d9457..9732d43 100644 --- a/example/src/examples/focus-slot.tsx +++ b/example/src/examples/focus-slot.tsx @@ -6,9 +6,8 @@ import Animated, { withRepeat, withTiming, withSequence, - useSharedValue, + useDerivedValue, } from 'react-native-reanimated'; -import { useEffect } from 'react'; export default function FocusSlotExample() { return ( @@ -17,8 +16,8 @@ export default function FocusSlotExample() { maxLength={6} render={({ slots }) => ( - {slots.map((slot, index) => ( - + {slots.map((slot, slotIndex) => ( + ))} )} @@ -41,18 +40,18 @@ function Slot({ char, isActive, hasFakeCaret, focus }: SlotProps) { } function FakeCaret() { - const opacity = useSharedValue(1); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/examples/revolt-nativewind.tsx b/example/src/examples/revolt-nativewind.tsx index f6c2832..af6c87c 100644 --- a/example/src/examples/revolt-nativewind.tsx +++ b/example/src/examples/revolt-nativewind.tsx @@ -8,9 +8,8 @@ import Animated, { withRepeat, withTiming, withSequence, - useSharedValue, + useDerivedValue, } from 'react-native-reanimated'; -import { useEffect } from 'react'; import { cn } from './utils'; export default function RevoltOTPInput() { @@ -69,18 +68,18 @@ function FakeDash() { } function FakeCaret() { - const opacity = useSharedValue(1); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/examples/revolt.tsx b/example/src/examples/revolt.tsx index ce0e0b2..176b588 100644 --- a/example/src/examples/revolt.tsx +++ b/example/src/examples/revolt.tsx @@ -15,9 +15,8 @@ import Animated, { withRepeat, withTiming, withSequence, - useSharedValue, + useDerivedValue, } from 'react-native-reanimated'; -import { useEffect } from 'react'; export default function RevoltOTPInput() { const ref = useRef(null); @@ -69,18 +68,18 @@ function FakeDash() { } function FakeCaret({ style }: { style?: ViewStyle }) { - const opacity = useSharedValue(1); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/examples/stripe-nativewind.tsx b/example/src/examples/stripe-nativewind.tsx index 4683a90..c4651b6 100644 --- a/example/src/examples/stripe-nativewind.tsx +++ b/example/src/examples/stripe-nativewind.tsx @@ -10,6 +10,7 @@ import Animated, { withTiming, withSequence, useSharedValue, + useDerivedValue, withSpring, } from 'react-native-reanimated'; import { useEffect } from 'react'; @@ -30,14 +31,14 @@ export default function StripeOTPInput() { render={({ slots }) => ( - {slots.slice(0, 3).map((slot, idx) => ( - + {slots.slice(0, 3).map((slot, slotIndex) => ( + ))} - {slots.slice(3).map((slot, idx) => ( - + {slots.slice(3).map((slot, slotIndex) => ( + ))} @@ -109,18 +110,18 @@ function FakeDash() { } function FakeCaret() { - const opacity = useSharedValue(1); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/examples/stripe.tsx b/example/src/examples/stripe.tsx index ebed58b..076b00d 100644 --- a/example/src/examples/stripe.tsx +++ b/example/src/examples/stripe.tsx @@ -15,9 +15,8 @@ import Animated, { withRepeat, withTiming, withSequence, - useSharedValue, + useDerivedValue, } from 'react-native-reanimated'; -import { useEffect } from 'react'; export default function StripeOTPInput() { const ref = useRef(null); @@ -34,14 +33,14 @@ export default function StripeOTPInput() { render={({ slots }) => ( - {slots.slice(0, 3).map((slot, idx) => ( - + {slots.slice(0, 3).map((slot, slotIndex) => ( + ))} - {slots.slice(3).map((slot, idx) => ( - + {slots.slice(3).map((slot, slotIndex) => ( + ))} @@ -85,18 +84,18 @@ function FakeDash() { } function FakeCaret({ style }: { style?: ViewStyle }) { - const opacity = useSharedValue(1); - - useEffect(() => { - opacity.value = withRepeat( - withSequence( - withTiming(0, { duration: 500 }), - withTiming(1, { duration: 500 }) + const opacity = useDerivedValue( + () => + withRepeat( + withSequence( + withTiming(0, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true ), - -1, - true - ); - }, [opacity]); + [] + ); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/example/src/title.tsx b/example/src/title.tsx deleted file mode 100644 index 6102296..0000000 --- a/example/src/title.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Text, StyleSheet } from 'react-native'; - -interface TitleProps { - children: string; -} - -export function Title({ children }: TitleProps) { - return {children}; -} - -const styles = StyleSheet.create({ - title: { - fontSize: 16, - marginBottom: 16, - color: 'gray', - textAlign: 'center', - }, -}); diff --git a/yarn.lock b/yarn.lock index 2ac857a..f317be8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10610,6 +10610,7 @@ __metadata: react-native: 0.79.2 react-native-builder-bob: ^0.35.2 react-native-reanimated: ~3.17.4 + react-native-safe-area-context: 5.4.0 react-native-web: ^0.20.0 tailwind-merge: ^2.6.0 tailwindcss: ^3.4.17 @@ -16324,6 +16325,16 @@ __metadata: languageName: node linkType: hard +"react-native-safe-area-context@npm:5.4.0": + version: 5.4.0 + resolution: "react-native-safe-area-context@npm:5.4.0" + peerDependencies: + react: "*" + react-native: "*" + checksum: 7d7f9a8278048650fd207d436798bd062d6f78d771cb0665b92aef69dba870251339e6812e1d669fd1958345288bd0f9ac98fbfe353c13958d18b58dc946c341 + languageName: node + linkType: hard + "react-native-web@npm:^0.20.0": version: 0.20.0 resolution: "react-native-web@npm:0.20.0"