Effect

PreviousNext

An effect that allows you to animate elements on first view or load.

Docs
animate-uiui

Preview

Loading preview…
registry/primitives/effects/effect/index.tsx
'use client';

import * as React from 'react';
import { motion, type HTMLMotionProps, type Variant } from 'motion/react';

import {
  useIsInView,
  type UseIsInViewOptions,
} from '@/hooks/use-is-in-view';
import { Slot, type WithAsChild } from '@/components/animate-ui/primitives/animate/slot';

type SlideDirection = 'up' | 'down' | 'left' | 'right';

type Slide = {
  direction?: SlideDirection;
  offset?: number;
};

type Fade = { initialOpacity?: number; opacity?: number };

type Zoom = {
  initialScale?: number;
  scale?: number;
};

type Blur = {
  initialBlur?: number;
  blur?: number;
};

type EffectProps = WithAsChild<
  {
    children?: React.ReactNode;
    delay?: number;
    blur?: Blur | boolean;
    slide?: Slide | boolean;
    fade?: Fade | boolean;
    zoom?: Zoom | boolean;
    ref?: React.Ref<HTMLElement>;
  } & UseIsInViewOptions &
    HTMLMotionProps<'div'>
>;

const DEFAULT_SLIDE_DIRECTION: SlideDirection = 'up';
const DEFAULT_SLIDE_OFFSET: number = 100;
const DEFAULT_FADE_INITIAL_OPACITY: number = 0;
const DEFAULT_FADE_OPACITY: number = 1;
const DEFAULT_ZOOM_INITIAL_SCALE: number = 0.5;
const DEFAULT_ZOOM_SCALE: number = 1;
const DEFAULT_BLUR_INITIAL_BLUR: number = 10;
const DEFAULT_BLUR_BLUR: number = 0;

function Effect({
  ref,
  transition = { type: 'spring', stiffness: 200, damping: 20 },
  delay = 0,
  inView = false,
  inViewMargin = '0px',
  inViewOnce = true,
  blur = false,
  slide = false,
  fade = false,
  zoom = false,
  asChild = false,
  ...props
}: EffectProps) {
  const { ref: localRef, isInView } = useIsInView(
    ref as React.Ref<HTMLElement>,
    {
      inView,
      inViewOnce,
      inViewMargin,
    },
  );

  const hiddenVariant: Variant = {};
  const visibleVariant: Variant = {};

  if (slide) {
    const offset =
      typeof slide === 'boolean'
        ? DEFAULT_SLIDE_OFFSET
        : (slide.offset ?? DEFAULT_SLIDE_OFFSET);
    const direction =
      typeof slide === 'boolean'
        ? DEFAULT_SLIDE_DIRECTION
        : (slide.direction ?? DEFAULT_SLIDE_DIRECTION);
    const axis = direction === 'up' || direction === 'down' ? 'y' : 'x';
    hiddenVariant[axis] =
      direction === 'right' || direction === 'down' ? -offset : offset;
    visibleVariant[axis] = 0;
  }

  if (fade) {
    hiddenVariant.opacity =
      typeof fade === 'boolean'
        ? DEFAULT_FADE_INITIAL_OPACITY
        : (fade.initialOpacity ?? DEFAULT_FADE_INITIAL_OPACITY);
    visibleVariant.opacity =
      typeof fade === 'boolean'
        ? DEFAULT_FADE_OPACITY
        : (fade.opacity ?? DEFAULT_FADE_OPACITY);
  }

  if (zoom) {
    hiddenVariant.scale =
      typeof zoom === 'boolean'
        ? DEFAULT_ZOOM_INITIAL_SCALE
        : (zoom.initialScale ?? DEFAULT_ZOOM_INITIAL_SCALE);
    visibleVariant.scale =
      typeof zoom === 'boolean'
        ? DEFAULT_ZOOM_SCALE
        : (zoom.scale ?? DEFAULT_ZOOM_SCALE);
  }

  if (blur) {
    hiddenVariant.filter =
      typeof blur === 'boolean'
        ? `blur(${DEFAULT_BLUR_INITIAL_BLUR}px)`
        : `blur(${blur.initialBlur ?? DEFAULT_BLUR_INITIAL_BLUR}px)`;
    visibleVariant.filter =
      typeof blur === 'boolean'
        ? `blur(${DEFAULT_BLUR_BLUR}px)`
        : `blur(${blur.blur ?? DEFAULT_BLUR_BLUR}px)`;
  }

  const Component = asChild ? Slot : motion.div;

  return (
    <Component
      ref={localRef as React.Ref<HTMLDivElement>}
      initial="hidden"
      animate={isInView ? 'visible' : 'hidden'}
      exit="hidden"
      variants={{
        hidden: hiddenVariant,
        visible: visibleVariant,
      }}
      transition={{
        ...transition,
        delay: (transition?.delay ?? 0) + delay / 1000,
      }}
      {...props}
    />
  );
}

type EffectsProps = Omit<EffectProps, 'children'> & {
  children: React.ReactElement | React.ReactElement[];
  holdDelay?: number;
};

function Effects({
  children,
  delay = 0,
  holdDelay = 0,
  ...props
}: EffectsProps) {
  const array = React.Children.toArray(children) as React.ReactElement[];

  return (
    <>
      {array.map((child, index) => (
        <Effect
          key={child.key ?? index}
          delay={delay + index * holdDelay}
          {...props}
        >
          {child}
        </Effect>
      ))}
    </>
  );
}

export {
  Effect,
  Effects,
  type EffectProps,
  type EffectsProps,
  type SlideDirection,
  type Slide,
  type Fade,
  type Zoom,
};

Installation

npx shadcn@latest add @animate-ui/primitives-effects-effect

Usage

import { PrimitivesEffectsEffect } from "@/components/ui/primitives-effects-effect"
<PrimitivesEffectsEffect />