npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@renegades/react-native-tickle

v0.1.2

Published

Build and fine-tune haptic patterns with real-time previews

Downloads

6,762

Readme

@renegades/react-native-tickle

AHAP-style haptics (transient + continuous) on top of Nitro Modules — the core functions are UI-thread friendly ('worklet').

Why this package?

Complete flexibility over iOS haptics with synchronous UI-thread execution.

iOS only (Core Haptics). Android support could be added in the future, but it's currently unplanned.

Installation

npm i @renegades/react-native-tickle react-native-nitro-modules

Concepts (what to use when)

  • Transient: Instant "click/tap" events. No duration — you trigger them at a point in time.
  • Continuous (pattern): Time-based patterns you can define ahead of time. You provide events (with duration) and optionally curves (automation over time).
  • Continuous player (real-time): For unpredictable input (gesture position, scroll velocity, real-time data). You create a player once, then start → update (many times) → stop.

Why events[] and curves[] are separate

On iOS Core Haptics, a pattern is made of two different building blocks:

  • Events: things that happen (transient "ticks" or continuous segments) at a relativeTime, with base intensity/sharpness.
  • Curves: how parameters (intensity/sharpness) evolve over time via control points, independent of "what event" is currently playing.

They're separate because they're different object types in Core Haptics (events vs parameter curves) and they serve different jobs: events define the structure, curves define the modulation. You often combine both in one pattern.

Haptix Studio

Try out Haptix Studio to see what's possible with haptics.

Play with continuous patterns and real-time updates, then export your creations directly into code.

Download on Test Flight

Usage

Haptic "provider" (recommended)

Wrap your app inside HapticProvider. This initializes the engine and automatically destroys it when the app goes to background.

import { HapticProvider } from '@renegades/react-native-tickle';

export function App() {
  return <HapticProvider>{/* {Rest of your app} */}</HapticProvider>;
}

startHaptic(events, curves) (transient + continuous patterns)

Play a transient:

import { startHaptic } from '@renegades/react-native-tickle';

startHaptic(
  [
    {
      type: 'transient',
      relativeTime: 0,
      parameters: [
        { type: 'intensity', value: 1 },
        { type: 'sharpness', value: 0.5 },
      ],
    },
  ],
  []
);

Play a continuous pattern (events + curves together):

import { startHaptic } from '@renegades/react-native-tickle';

startHaptic(
  [
    {
      type: 'continuous',
      relativeTime: 0,
      duration: 1200,
      parameters: [
        { type: 'intensity', value: 0.2 },
        { type: 'sharpness', value: 0.5 },
      ],
    },
  ],
  [
    {
      type: 'intensity',
      relativeTime: 0,
      controlPoints: [
        { relativeTime: 0, value: 0.2 },
        { relativeTime: 600, value: 1.0 },
        { relativeTime: 1200, value: 0.2 },
      ],
    },
  ]
);

Combine transient + continuous in one pattern:

import { startHaptic } from '@renegades/react-native-tickle';

startHaptic(
  [
    {
      type: 'transient',
      relativeTime: 0,
      parameters: [
        { type: 'intensity', value: 1 },
        { type: 'sharpness', value: 0.8 },
      ],
    },
    {
      type: 'continuous',
      relativeTime: 50,
      duration: 800,
      parameters: [
        { type: 'intensity', value: 0.2 },
        { type: 'sharpness', value: 0.4 },
      ],
    },
  ],
  []
);

Real-time continuous mode (continuous player)

Use this when you can't predefine a pattern. You start the player, update it in real time, then stop it.

import { useContinuousPlayer } from '@renegades/react-native-tickle';

function MyComponent() {
  const { start, stop, update } = useContinuousPlayer('my-player', 1.0, 0.5);

  const gesture = Gesture.Pan()
    .onBegin(() => {
      start();
    })
    .onUpdate((e) => {
      update(e.translationY / 100, 0.5);
    })
    .onEnd(() => {
      stop();
    });
}

Stop everything

If users can navigate away from a screen while haptics are still playing, call stopAllHaptics() in your cleanup to stop them.

import { stopAllHaptics } from '@renegades/react-native-tickle';
import { useEffect } from 'react';

export function SomeScreen() {
  useEffect(() => () => stopAllHaptics(), []);
  return null;
}

Global enable/disable (settings toggle)

Disable haptics globally for users who prefer no haptic feedback. The setting is persisted across app restarts. When disabled, all haptic calls become no-ops.

import { useHapticsEnabled } from '@renegades/react-native-tickle';

function SettingsScreen() {
  const [hapticsEnabled, setHapticsEnabled] = useHapticsEnabled();

  return <Switch value={hapticsEnabled} onValueChange={setHapticsEnabled} />;
}

System haptics (predefined OS-level feedback)

While the main purpose of this package is to support AHAP-style patterns (transient + continuous haptics with curves), system haptics are also available for completeness. These are simple, predefined OS-level feedback types that don't require pattern definitions. These methods are also UI-thread friendly.

import {
  triggerImpact,
  triggerNotification,
  triggerSelection,
} from '@renegades/react-native-tickle';

// Impact feedback - simulates a physical collision
triggerImpact('light'); // 'light' | 'medium' | 'heavy' | 'soft' | 'rigid'

// Notification feedback - for alerts and status updates
triggerNotification('success'); // 'success' | 'warning' | 'error'

// Selection feedback - for picker wheels and toggles
triggerSelection();

Advanced control

For fine-grained control over the haptic API, you can opt out of the managed hooks and call the underlying functions directly:

  • Engine lifecycle: initializeEngine() / destroyEngine() instead of HapticProvider
  • Enable/disable toggle: setHapticsEnabled() / getHapticsEnabled() instead of useHapticsEnabled()
  • Continuous player: createContinuousPlayer() / startContinuousPlayer() / updateContinuousPlayer() / stopContinuousPlayer() / destroyContinuousPlayer() instead of useContinuousPlayer()

API

| Function | Purpose | | ---------------------------------------------------------------------- | --------------------------------------------------------------- | | HapticProvider | Component that initializes engine; destroys on background | | useHapticEngine() | Hook to manage engine lifecycle (used internally by provider) | | initializeEngine() / destroyEngine() | Manual engine lifecycle | | startHaptic(events, curves) | Play a pattern (transient + continuous events, optional curves) | | stopAllHaptics() | Stop any running haptics (useful on unmount/navigation) | | useHapticsEnabled() | Hook for reactive haptics enabled state | | setHapticsEnabled(enabled) / getHapticsEnabled() | Manual enable/disable toggle (persisted) | | useContinuousPlayer(playerId, initialIntensity, initialSharpness) | Hook to manage a continuous player lifecycle | | createContinuousPlayer(playerId, initialIntensity, initialSharpness) | Create a continuous player with given ID | | startContinuousPlayer(playerId) / stopContinuousPlayer(playerId) | Start/stop continuous playback for player | | updateContinuousPlayer(playerId, intensityControl, sharpnessControl) | Update intensity/sharpness for player | | destroyContinuousPlayer(playerId) | Destroy player and release resources | | triggerImpact(style) | Trigger impact feedback (collision simulation) | | triggerNotification(type) | Trigger notification feedback (alerts/status) | | triggerSelection() | Trigger selection feedback (pickers/toggles) |

Types (inputs)

| Type | Values | | ------------------------ | ----------------------------------------------------- | | HapticParameterType | 'intensity' \| 'sharpness' | | HapticImpactStyle | 'light' \| 'medium' \| 'heavy' \| 'soft' \| 'rigid' | | HapticNotificationType | 'success' \| 'warning' \| 'error' |

| HapticEventParameter | Type | | ---------------------- | --------------------- | | type | HapticParameterType | | value | number |

HapticEvent (discriminated union)

  • Transient:

    • type: 'transient'
    • relativeTime: number
    • parameters: HapticEventParameter[]
  • Continuous:

    • type: 'continuous'
    • relativeTime: number
    • duration: number
    • parameters: HapticEventParameter[]

| HapticCurveControlPoint | Type | | ------------------------- | -------- | | relativeTime | number | | value | number |

| HapticCurve | Type | | --------------- | --------------------------- | | type | HapticCurveType | | relativeTime | number | | controlPoints | HapticCurveControlPoint[] |

Limitations

Parameter curves affect all events in a pattern

In CoreHaptics, CHHapticParameterCurve uses hapticIntensityControl and hapticSharpnessControl — these are pattern-level multipliers, not per-event modifiers. Any curve you define will multiply the intensity/sharpness of all events playing at that moment, including transients.

Example problem:

startHaptic(
  [
    { type: 'continuous', relativeTime: 0, duration: 2000, parameters: [...] },
    { type: 'transient', relativeTime: 1000, parameters: [{ type: 'intensity', value: 1.0 }, ...] },
  ],
  [
    { type: 'intensity', relativeTime: 0, controlPoints: [
      { relativeTime: 0, value: 1.0 },
      { relativeTime: 1000, value: 0.3 },  // At t=1000ms, intensity control = 0.3
      { relativeTime: 2000, value: 0.3 },
    ]},
  ]
);

The transient at t=1000ms has base intensity 1.0, but the curve sets intensityControl=0.3 at that moment. Effective intensity: 1.0 × 0.3 = 0.3. The transient feels weaker than expected.

Workaround: Play continuous and transient events in separate startHaptic() calls:

// Continuous with curves
startHaptic(continuousEvents, curves);

// Transients without curves (separate pattern)
startHaptic(transientEvents, []);

Each call creates an isolated pattern/player — curves from one won't affect events in the other.

Note: The library automatically resets control values to 1.0 at the end of each continuous event, so transients after a continuous event finishes are not affected. This limitation only applies to transients during a continuous event with active curves.

Acknowledgment

Shoutout to Jai for kicking off the Swift implementation 🙌

Contributing

License

MIT