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

react-native-smooth-clip-view

v0.5.1

Published

Layout-free animated rounded clipping for React Native Fabric

Readme

react-native-smooth-clip-view

Layout-free animated rounded clipping for React Native Fabric.

SmoothClipView is a fixed viewport. Its controller moves a rounded aperture inside that viewport using raw host-local coordinates. Content is clipped by the aperture, an optional outset shadow may extend outside the aperture, and the host finally crops both. A full-screen host therefore accepts screen coordinates directly, including negative and off-screen frames.

Use this library when a transition needs streamed gesture updates followed by native-owned timing or spring motion without animating Yoga layout. A regular Reanimated View with overflow: 'hidden' remains the simpler choice when layout-thread work and interruption snapshots are not concerns.

Demo

https://github.com/user-attachments/assets/899235b3-de69-46d7-b6db-61bc54d80df8

Requirements

  • React Native 0.86 or newer with the New Architecture
  • React 19.2 or newer
  • React Native Reanimated 4.5 or newer
  • React Native Worklets 0.10.1 or newer
  • Android API 24 or newer
npm install react-native-smooth-clip-view react-native-reanimated react-native-worklets
cd ios && pod install

One clip

import {
  ClipEasings,
  SmoothClipView,
  useSmoothClipController,
} from 'react-native-smooth-clip-view';

function Reveal() {
  const clip = useSmoothClipController({
    clip: { x: 24, y: 80, width: 96, height: 96, radius: 24 },
    contentTranslateX: -24,
    contentTranslateY: -80,
    contentScale: 1,
  });

  const open = () => {
    clip.react.animateTo(
      {
        clip: { x: 0, y: 0, width: 390, height: 844, radius: 0 },
        contentTranslateX: 0,
        contentTranslateY: 0,
        contentScale: 1,
      },
      {
        type: 'timing',
        duration: 400,
        controlPoints: ClipEasings.easeOutCubic,
      }
    );
  };

  return (
    <SmoothClipView controller={clip} style={{ flex: 1 }}>
      {/* screen-sized transition content */}
    </SmoothClipView>
  );
}

One controller may have one mounted host at a time. Sequential unmount and remount is supported. In development, mounting the same controller in two hosts throws an error.

Worklet API

Call clip.ui methods from a UI-runtime worklet:

clip.ui.setFrame(presentation);

const run = clip.ui.animateTo(target, spring, 1);

clip.ui.cancel(run); // optional; freezes at the visible frame

Native terminal events are delivered on the React JavaScript runtime through one stable controller callback. Consumer callbacks are never retained in the UI runtime:

const clip = useSmoothClipController(initialPresentation, {
  onAnimationComplete(result) {
    // result.completionTag is 1 for the tagged run above
    // result.finished is true only when the target was reached
  },
});

The optional completion tag is a nonnegative 32-bit integer. Use it to identify UI-runtime runs without retaining a consumer worklet for the animation lifetime.

setFrame canonicalizes the object in the worklet and makes one scalar JSI call. It does not update React state, animated props, Yoga, the ShadowTree, or a public SharedValue.

beginInteraction() atomically interrupts native motion and returns its visible raw presentation. Apply the final gesture sample with setFrame() before calling it when the release must start from that exact frame.

React API

React-thread animation starts synchronously return a run object:

const run = clip.react.animateTo(target, {
  type: 'spring',
  stiffness: 180,
  damping: 22,
  velocity: 1.4,
});

// run.cancel(); // optional; freezes the visible frame and resolves false
const finished = await run.finished;

Timing accepts duration and cubic-Bezier controlPoints. Spring accepts optional mass, stiffness, damping, normalized velocity, and energyThreshold. Both specifications accept reduceMotion. The physical defaults match Reanimated 4.5: mass 4, stiffness 900, damping 120, and relative energy threshold 6e-9. A spring is rejected when its resolved native trajectory could make contentScale nonpositive.

velocity is a shared initial progress velocity in inverse seconds, defaulting to 0. Each changing channel starts with velocity proportional to its remaining distance: velocity * (target - start). This includes rotation (in radians) and opacity. Public controllers and groups do not automatically inherit gesture velocity; starting a new spring after beginInteraction() still defaults to 0 unless velocity is supplied. Pausing and resuming an existing native spring preserves its internal motion state; public snapshots return clamped opacity.

The internal native standalone path also supports inherited velocity. Its projection uses only clip position, size, corner radii, and content translation. Rotation, opacity, content scale, and shadow channels do not contribute to that projection. The resulting normalized velocity can drive all changing channels, including rotation and opacity, without mixing their units into the projection. Rotation/opacity-only motion therefore supplies no inherited velocity.

An animation requested before the host has a positive layout waits, then starts with its full duration. While the application is inactive, native animation transactions stay pending and do not report cancellation. Timing animations retain wall-clock progress and catch up after activation. Springs stay frozen; their first foreground step consumes at most 64 ms, matching Reanimated, and then they continue normally. If the platform discarded an underlying animation object, the transaction settles successfully at its requested target.

Interactive setFrame() updates remain application-owned. The library does not reset a drag or mutate related SharedValues or React state when lifecycle state changes, so applications do not need lifecycle integration for the clip itself.

Host loss while the application is active keeps the existing semantics: standalone animations apply their target and finish successfully, while groups (including public controllers because they use one-member groups) freeze every member and resolve false. Replacement, cancellation, destruction, and rejection also settle once with false.

Atomic groups

Store controller.ref when multiple clips must update or settle atomically:

const group = useSmoothClipGroup();

group.ui.setFrames([
  { clip: first.ref, frame: firstFrame },
  { clip: second.ref, frame: secondFrame },
]);

const snapshots = group.ui.beginInteraction([first.ref, second.ref]);

const run = group.react.animateTo(
  [
    { clip: first.ref, target: firstTarget },
    { clip: second.ref, target: secondTarget },
  ],
  { type: 'timing', duration: 320, controlPoints: ClipEasings.easeOutCubic }
);

const result = await run.finished;

group.ui.setFrames uses one native batch call. beginInteraction snapshots and cancel snapshots preserve input order and report whether each clip currently has a displayable host. Controllers are implemented as one-member groups, so both APIs share validation, run ownership, and completion behavior.

Geometry and rendering

  • x and y are never intersected with the host.
  • Negative width and height canonicalize to zero.
  • Corner-overlap scaling follows CSS rules against the requested rectangle.
  • contentTranslateX, contentTranslateY, and positive contentScale animate with the aperture.
  • rotation accepts degree or radian strings (for example, '12deg' or '0.2rad') and defaults to '0deg'. It rotates the aperture, content, and shadow together, clockwise around the current aperture center. Existing content translation and scale happen before this rotation. The host stays fixed and crops the rotated result.
  • opacity defaults to 1 and fades the aperture's content and shadow as one group. Finite values clamp to [0, 1]; malformed angles and nonfinite values reject the complete presentation. Spring opacity is clamped for rendering without clamping its internal spring state.
  • Circular and continuous curves and independent corner radii are supported.
  • continuous is the platform squircle: CALayer.cornerCurve on iOS for uniform radii, and a Figma smoothed corner at smoothing 0.6 on Android (the shape react-native-fast-squircle draws at cornerSmoothing={0.6}). The apex matches a circular corner of the same radius; the shoulders start 1.6 × the radius from the corner. The two platforms are close, not pixel-identical.
  • Native animateTo runs need uniform corner radii and one curve for the whole run, so a uniform continuous clip animates natively. Unequal radii or a curve change are static-only: set them with setFrame; animateTo returns null.
  • One outset boxShadow is supported. It escapes the aperture but not the host.
  • shadowRendering="baked" on the host draws that shadow from one pre-blurred tile per corner-radius step (4 pt), stretched by the compositor, instead of blurring a path on every frame the aperture moves: a native run or a drag of a full-screen window then costs no per-frame blur on either platform. A tile is baked once per (radius step, blur, colour), about a millisecond for a 16 pt blur, and cached for the process. Only two bakes happen on the calling thread: a shadow's first tile (at mount, when nothing else can be shown) and a run's resting target (at install, so the run ends exact). Every other step bakes off the frame while the nearest cached step stands in, so a drag that crosses radius steps never blocks a frame. It needs uniform corner radii and a shape at least 2 × (1.5 × blur + radius) on each side, room for two corner slices (unequal radii or a smaller shape keep the blur path for that frame or run); the tile corner rounds up to the next step, so a shadow corner may be up to 4 pt rounder than its clip, which no blur the tile was made for resolves; a run that changes radius, blur or colour swaps tiles in steps along the way. The baked shadow is drawn under the aperture on both platforms (the Android blur path cuts it out), so content over it should be opaque.
  • backdrop: { translateX, translateY } on a presentation translates every SmoothClipBackdropView bound to the same controller (controller={clip}, anywhere in the tree, any number of them). The channel is part of the presentation, so a setFrame writes it in the same native call as the clip and a run animates it on the clip's own epoch: content that must stay locked to the aperture (a screen-sized canvas centred in the window) is sampled by the same clock as the clip, with no Reanimated mapper in between. The translation goes on an inner content layer; the view's own transform style stays React Native's. A backdrop that binds while a run is in flight adopts the driver's current value and follows from the next run. While no backdrop is bound the channel is not interpolated: a snapshot or freeze taken mid-run reports the run's target translation, which is what a backdrop binding at that moment adopts.
  • A fully off-host aperture is not touchable, even when only its shadow overlaps.
  • Descendant accessibility is hidden during autonomous native motion and restored from aperture/host intersection at the endpoint.

Use getSmoothClipCapabilities() when a consumer needs to decide whether a complex native path can be promoted on the current platform.

Rotate and fade a clip

Rotation and opacity are presentation fields, so they use the same controller and group methods as geometry. For example, start a tilted, translucent card and animate it upright:

const clip = useSmoothClipController({
  clip: { x: 24, y: 80, width: 240, height: 320, radius: 24 },
  contentTranslateX: 0,
  contentTranslateY: 0,
  rotation: '-12deg',
  opacity: 0.4,
});

// Attach this controller to <SmoothClipView controller={clip}> as above.
const reveal = () =>
  clip.react.animateTo(
    {
      clip: { x: 24, y: 80, width: 240, height: 320, radius: 24 },
      contentTranslateX: 0,
      contentTranslateY: 0,
      rotation: '0deg',
      opacity: 1,
    },
    { type: 'spring', stiffness: 180, damping: 22 }
  );

For gesture-driven updates, supply the same fields from a UI-runtime worklet:

clip.ui.setFrame({ ...frame, rotation: '12deg', opacity: 0.5 });

Angles interpolate numerically, so '720deg' requests two full turns from zero; there is no automatic shortest-path wrapping. Interruption snapshots preserve full turns and return rotation as a radian string. Each presentation is complete: omitting rotation or opacity resets that field to its default. At zero opacity, the content admits no new touches and is hidden from accessibility.

When upgrading to 0.4.3, rebuild the native app (and run pod install on iOS). The native presentation protocol includes new rotation and opacity channels, so a JavaScript-only OTA update cannot upgrade an older native build. See the changelog for release details.

Performance contract

  • One worklet-to-native call per ui.setFrame, or per group batch.
  • Native timing and springs run without JS work between start and completion.
  • A run started inside a UI frame is anchored to that frame's Reanimated stamp (__frameTimestamp) on both platforms, so it shares one epoch with a withTiming begun in the same frame.
  • A host with no run in flight and no setFrame stream costs nothing per frame: iOS keeps no display link, and Android posts its frame callback only while a run animates. Static content (a list of cards, say) can be decorated with a host at no per-frame cost, and hand off to an animated host through the same path builder and shadow renderer.
  • Content driven by Reanimated inside the host (an Animated.View with a useAnimatedStyle) can land a frame after the clip on iOS: Reanimated applies mapper props in its own display-link pass, separate from the worklet frame that computed them, while a native run is sampled by Core Animation for the same vsync. A main-thread overrun widens that to a frame either way. Anything that must stay locked to the aperture belongs on the run itself: put it in a SmoothClipBackdropView and drive it through the presentation's backdrop channel.
  • The shadow-disabled rendering path keeps no shadow drawing resources.
  • The fixed host is the maximum rendering viewport; consumers should size it to the region in which content and shadow may appear.
  • Rotation and opacity update native compositor properties without Yoga work or rebuilding unchanged clipping paths. Correctly fading overlapping children may require offscreen compositing; large translucent groups can cost more GPU time than opaque ones. No rasterization or software layer is forced.

License

MIT