Motion Grid

PreviousNext

A grid that displays animations in a grid.

Docs
animate-uiui

Preview

Loading preview…
registry/primitives/animate/motion-grid/index.tsx
'use client';

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

import { cn } from '@/lib/utils';
import { getStrictContext } from '@/lib/get-strict-context';
import { Slot, type WithAsChild } from '@/components/animate-ui/primitives/animate/slot';

type FrameDot = [number, number];
type Frame = FrameDot[];
type Frames = Frame[];

type MotionGridContextType = {
  index: number;
  cols: number;
  rows: number;
  frames: Frames;
  duration: number;
  animate: boolean;
};

const [MotionGridProvider, useMotionGrid] =
  getStrictContext<MotionGridContextType>('MotionGridContext');

type MotionGridProps = WithAsChild<
  {
    gridSize: [number, number];
    frames: Frames;
    duration?: number;
    animate?: boolean;
  } & HTMLMotionProps<'div'>
>;

const MotionGrid = ({
  gridSize,
  frames,
  duration = 200,
  animate = true,
  asChild = false,
  style,
  ...props
}: MotionGridProps) => {
  const [index, setIndex] = React.useState(0);
  const intervalRef = React.useRef<NodeJS.Timeout | null>(null);

  React.useEffect(() => {
    if (!animate || frames.length === 0) return;
    intervalRef.current = setInterval(
      () => setIndex((i) => (i + 1) % frames.length),
      duration,
    );
    return () => clearInterval(intervalRef.current!);
  }, [frames.length, duration, animate]);

  const [cols, rows] = gridSize;

  const Component = asChild ? Slot : motion.div;

  return (
    <MotionGridProvider
      value={{ animate, index, cols, rows, frames, duration }}
    >
      <Component
        data-animate={animate}
        style={{
          display: 'grid',
          gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
          gridAutoRows: '1fr',
          ...style,
        }}
        {...props}
      />
    </MotionGridProvider>
  );
};

type MotionGridCellsProps = HTMLMotionProps<'div'> & {
  activeProps?: HTMLMotionProps<'div'>;
  inactiveProps?: HTMLMotionProps<'div'>;
};

function MotionGridCells({
  activeProps,
  inactiveProps,
  ...props
}: MotionGridCellsProps) {
  const { animate, index, cols, rows, frames, duration } = useMotionGrid();

  const active = new Set<number>(
    frames[index]?.map(([x, y]) => y * cols + x) ?? [],
  );

  return Array.from({ length: cols * rows }).map((_, i) => {
    const isActive = active.has(i);
    const componentProps: HTMLMotionProps<'div'> = {
      ...(isActive ? activeProps : inactiveProps),
    };
    componentProps.className = cn(
      props?.className,
      isActive ? activeProps?.className : inactiveProps?.className,
    );
    componentProps.style = {
      ...props?.style,
      ...(isActive ? activeProps?.style : inactiveProps?.style),
    };

    return (
      <motion.div
        key={i}
        data-active={isActive}
        data-animate={animate}
        transition={{ duration, ease: 'easeInOut' }}
        {...props}
        {...componentProps}
      />
    );
  });
}

export {
  MotionGrid,
  MotionGridCells,
  type MotionGridProps,
  type MotionGridCellsProps,
  type FrameDot,
  type Frame,
  type Frames,
};

Installation

npx shadcn@latest add @animate-ui/primitives-animate-motion-grid

Usage

import { PrimitivesAnimateMotionGrid } from "@/components/ui/primitives-animate-motion-grid"
<PrimitivesAnimateMotionGrid />