Skip to content

About

Smooth, 120 FPS infinite looping time picker for React Native & Expo with zero external dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

react-native-loop-time-picker

Smooth, 120 FPS infinite looping time picker for React Native & Expo with zero external dependencies.

React Native Loop Time Picker Demo

npm version License: MIT Zero Dependencies Expo Compatible


Highlights

  • πŸ”„ Infinite Looping Runway: Seamless 55 -> 00 and 23 -> 00 rolling without jarring programmatic jumps or edge collisions.
  • ⚑ 120 FPS Native Motion: Pure native Animated driver with continuous scaling (44pt -> 34pt -> 26pt) and opacity cross-fade.
  • πŸ›‘οΈ Zero Native Node Leaks: Memoized row interpolation nodes created once on mount, preventing native driver memory leaks and scroll degradation ("tired wheels").
  • πŸ“¦ Zero External Dependencies: Built exclusively on core React Native Animated and ScrollView. Works seamlessly in Expo Go, Bare React Native, and React Native for Web without pod installs or Reanimated version conflicts.
  • πŸ•’ 12-Hour & 24-Hour Modes: 24h continuous clock or 12h with an AM/PM wheel.
  • 🎨 Dark / Light & Custom Theming: Out-of-the-box dark and light modes, customizable colors, font families, and item heights.
  • πŸͺŸ Inline & Modal Support: Use directly in your screen layout or as a pop-up modal.

Installation

# npm
npm install react-native-loop-time-picker

# yarn
yarn add react-native-loop-time-picker

# pnpm
pnpm add react-native-loop-time-picker

# expo
npx expo install react-native-loop-time-picker

No native pods or Gradle configuration required!


Quick Start

1. Inline TimePicker

import React, { useState } from 'react';
import { View } from 'react-native';
import { TimePicker } from 'react-native-loop-time-picker';

export default function App() {
  const [hour, setHour] = useState(14);
  const [minute, setMinute] = useState(30);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <TimePicker
        hour={hour}
        minute={minute}
        onTimeChange={(h, m) => {
          setHour(h);
          setMinute(m);
        }}
        is24Hour={true}
        minuteInterval={5}
        theme="dark" // or "light"
        title="Select Reminder Time"
      />
    </View>
  );
}

2. TimePicker in a Modal

import React, { useState } from 'react';
import { Button, View } from 'react-native';
import { TimePickerModal } from 'react-native-loop-time-picker';

export default function ModalExample() {
  const [hour, setHour] = useState(9);
  const [minute, setMinute] = useState(0);
  const [isOpen, setIsOpen] = useState(false);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Button title="Open Time Picker" onPress={() => setIsOpen(true)} />

      <TimePickerModal
        visible={isOpen}
        hour={hour}
        minute={minute}
        is24Hour={false} // 12-hour AM/PM mode
        minuteInterval={5}
        theme="dark"
        onConfirm={(newHour, newMinute) => {
          setHour(newHour);
          setMinute(newMinute);
          setIsOpen(false);
        }}
        onCancel={() => setIsOpen(false)}
        confirmText="Save"
        cancelText="Cancel"
      />
    </View>
  );
}

Why This Library? (The Engineering Story)

If you've ever tried to build an infinite loop picker in React Native, you know how deceitfully hard it is. Almost every existing approach suffers from critical flaws:

  1. The Instant Jump Trap (scrollTo({ animated: false })): Attempting to silently jump back to the middle of a list while momentum is active causes values to bounce wildly, numbers to freeze small between rows, and laggy headers.
  2. Virtualization Flash (FlatList): Virtualized lists cannot mount cells fast enough at high swipe velocities (10,000+ px/sec), creating blank white holes.
  3. The Native Driver Memory Leak ("Tired Wheels"): Calling scrollY.interpolate(...) inside list renders attaches hundreds of zombie native interpolation nodes to the native UI driver on every frame. Within a few seconds of spinning, the UI thread bogs down and drops to 15 FPS.

The Solution: Continuous Runway & Memoized Nodes

react-native-loop-time-picker solves this permanently:

  • Continuous Runway (LOOP_COPIES = 11): Instead of risky programmatic jumps during scroll, the wheel uses 11 repeating copies initialized at center (copy 5). Users get 5 full revolutions (>3,300 points of scroll / 60 items) in both directions. Native decelerationRate="fast" halts any swipe in 4–8 rows, making boundary collisions virtually impossible without requiring any jump hacks.
  • Memoized Native Nodes: Each row (WheelRow) is an isolated memoized component. Interpolation nodes are created once on mount per row and reused for the component lifetime: zero memory leaks, zero frame drops, consistent 120 FPS.
  • Graceful Snapping (findNearestIndex): If an unrounded minute (e.g. 23) is passed into a 5-minute interval picker, it smoothly maps to the nearest interval (25) without jumping back to 0.

Props Reference

<TimePicker />

Prop Type Default Description
hour number Required Current hour in 24-hour format (0–23).
minute number Required Current minute (0–59).
onTimeChange (hour: number, minute: number) => void Required Callback fired when hour or minute changes.
is24Hour boolean true When false, displays 12-hour clock (1–12) with an AM/PM selector.
minuteInterval number 5 Step interval for minutes (e.g., 1, 5, 10, 15).
loop boolean true Whether the wheels loop infinitely.
theme 'light' | 'dark' | TimePickerTheme 'light' Preset theme name or custom theme object.
title string undefined Optional title displayed above the live time readout.
showPreview boolean true Whether to show the header with live preview.
style ViewStyle undefined Custom container style.
testID string undefined Test identifier.

<TimePickerModal />

Inherits all <TimePicker /> props plus:

Prop Type Default Description
visible boolean Required Controls modal visibility.
onConfirm (hour: number, minute: number) => void Required Fired when user presses the confirm button.
onCancel () => void Required Fired when user dismisses the modal.
confirmText string 'Confirm' Text for the primary confirm button.
cancelText string 'Cancel' Text for the cancel button.
animationType 'none' | 'slide' | 'fade' 'fade' Modal transition animation.

Custom Theme Example

You can pass a custom theme object to match your design system:

<TimePicker
  hour={hour}
  minute={minute}
  onTimeChange={(h, m) => { setHour(h); setMinute(m); }}
  theme={{
    cardBackgroundColor: '#121212',
    activeTextColor: '#FFFFFF',
    inactiveTextColor: '#666666',
    highlightColor: '#262626',
    separatorColor: '#FFFFFF',
    fontFamily: 'Inter-Bold',
  }}
/>

Testing

Run unit tests verifying boundary continuity, 12h/24h conversions, and runway calculations:

npm test

License

MIT Β© Kirill Zolygin

About

Smooth, 120 FPS infinite looping time picker for React Native & Expo with zero external dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages