Veltrix

loading

Assembly Loader

A loading wrapper that assembles its children in sequence to preview the next view.

AppleiOSAndroidAndroidExpoExpo Go
reanimated
GitHubOpen in GitHub

Installation

Install the dependencies, then copy the files for your preferred renderer.

pnpm add react-native-reanimated

Install tailwind variants

pnpm add tailwind-variants

Usage

ProjectScreen.tsx
import { View } from "react-native";import { AssemblyLoader } from "@animations/ui/components/loading/assembly-loader/uniwind";export function ProjectScreen() {  return (    <AssemblyLoader contentClassName="gap-4">      <View className="h-6 w-1/2 rounded-lg bg-[#e5e1d8]" />      <View className="h-40 w-full rounded-3xl bg-[#e5e1d8]" />      <View className="h-12 w-full rounded-2xl bg-[#e5e1d8]" />    </AssemblyLoader>  );}

Component files

5 files

assembly-loader/config.ts
export const ASSEMBLY_LOADER_CONFIG = {  defaultDuration: 1_700,  defaultProgress: 0.68,  revealWindow: 0.18,} as const;export function getAssemblyThreshold(index: number, count: number) {  if (count <= 1) {    return 0.16;  }  return 0.08 + (index / (count - 1)) * 0.78;}
assembly-loader/types.ts
import type { ReactNode } from "react";export type AssemblyLoaderBaseProps = {  /** Accessible description announced while the next view is assembling. */  accessibilityLabel?: string;  /** Pieces of the next view, revealed in their child order. */  children: ReactNode;  /** Duration of the automatic assembly loop or controlled transition. */  duration?: number;  /** Controlled assembly state from 0 (empty) to 1 (ready). */  progress?: number;};
assembly-loader/uniwind/index.tsx
import { Children, type ReactNode } from "react";import { View } from "react-native";import Animated, {  type SharedValue,} from "react-native-reanimated";import type { SlotsToClasses } from "#shared/types/slots";import { getAssemblyThreshold } from "../config";import type { AssemblyLoaderBaseProps } from "../types";import {  normalizeAssemblyProgress,  useAssemblyBlockStyle,  useAssemblyLoader,} from "../use-assembly-loader";import {  assemblyLoaderVariants,  type AssemblyLoaderSlot,} from "./variants";export type AssemblyLoaderProps = AssemblyLoaderBaseProps & {  className?: string;  classNames?: SlotsToClasses<AssemblyLoaderSlot>;  contentClassName?: string;};function AssemblyChild({  child,  className,  index,  progress,  total,}: {  child: ReactNode;  className: string;  index: number;  progress: SharedValue<number>;  total: number;}) {  const animatedStyle = useAssemblyBlockStyle(    progress,    getAssemblyThreshold(index, total),  );  return (    <Animated.View className={className} style={animatedStyle}>      {child}    </Animated.View>  );}/** Wraps custom view pieces and assembles them in their child order. */export function AssemblyLoader({  accessibilityLabel = "Assembling next view",  children,  className,  classNames,  contentClassName,  duration,  progress: progressProp,}: AssemblyLoaderProps) {  const progress = useAssemblyLoader({    duration,    progress: progressProp,  });  const childNodes = Children.toArray(children);  const slots = assemblyLoaderVariants();  const accessibilityValue =    progressProp === undefined      ? undefined      : {          max: 1,          min: 0,          now: normalizeAssemblyProgress(progressProp),        };  return (    <View      accessibilityLabel={accessibilityLabel}      accessibilityRole="progressbar"      accessibilityState={{        busy:          progressProp === undefined ||          normalizeAssemblyProgress(progressProp) < 1,      }}      accessibilityValue={accessibilityValue}      accessible      className={slots.root({ className: [className, classNames?.root] })}    >      <View        className={slots.content({          className: [contentClassName, classNames?.content],        })}      >        {childNodes.map((child, index) => (          <AssemblyChild            child={child}            className={slots.child({ className: classNames?.child })}            index={index}            key={index}            progress={progress}            total={childNodes.length}          />        ))}      </View>    </View>  );}
assembly-loader/uniwind/variants.ts
import { tv } from "tailwind-variants";export const assemblyLoaderSlots = {  child: "relative",  content: "w-full gap-3",  root: "w-full overflow-hidden rounded-[28px] p-5",} as const;export type AssemblyLoaderSlot = keyof typeof assemblyLoaderSlots;export const assemblyLoaderVariants = tv({  slots: assemblyLoaderSlots,});
assembly-loader/use-assembly-loader.ts
import { useEffect } from "react";import {  cancelAnimation,  Easing,  useAnimatedStyle,  useReducedMotion,  useSharedValue,  withRepeat,  withTiming,  type SharedValue,} from "react-native-reanimated";import { ASSEMBLY_LOADER_CONFIG } from "./config";function clampProgress(progress: number) {  return Math.max(0, Math.min(1, progress));}export function normalizeAssemblyProgress(progress?: number) {  return clampProgress(    progress ?? ASSEMBLY_LOADER_CONFIG.defaultProgress,  );}export function useAssemblyLoader({  duration = ASSEMBLY_LOADER_CONFIG.defaultDuration,  progress,}: {  duration?: number;  progress?: number;}) {  const reducedMotion = useReducedMotion();  const assemblyProgress = useSharedValue(    normalizeAssemblyProgress(progress),  );  useEffect(() => {    cancelAnimation(assemblyProgress);    if (reducedMotion) {      if (progress !== undefined) {        assemblyProgress.set(normalizeAssemblyProgress(progress));      }      return;    }    if (progress === undefined) {      assemblyProgress.set(0);      assemblyProgress.set(        withRepeat(          withTiming(1, {            duration,            easing: Easing.inOut(Easing.cubic),          }),          -1,          true,        ),      );      return () => cancelAnimation(assemblyProgress);    }    assemblyProgress.set(      withTiming(normalizeAssemblyProgress(progress), {        duration,        easing: Easing.out(Easing.cubic),      }),    );    return () => cancelAnimation(assemblyProgress);  }, [    assemblyProgress,    duration,    progress,    reducedMotion,  ]);  return assemblyProgress;}export function useAssemblyBlockStyle(  progress: SharedValue<number>,  threshold: number,) {  return useAnimatedStyle(() => {    const reveal = Math.max(      0,      Math.min(        1,        (progress.get() -          Math.max(            0,            threshold - ASSEMBLY_LOADER_CONFIG.revealWindow,          )) /          ASSEMBLY_LOADER_CONFIG.revealWindow,      ),    );    return {      opacity: 0.28 + reveal * 0.72,      transform: [        { scale: 0.965 + reveal * 0.035 },        { translateY: 10 - reveal * 10 },      ],    };  });}

API reference

Generated directly from the exported component props.

AssemblyLoader

Wraps custom view pieces and assembles them in their child order.