All components

Animated Notification List

Animated Notification List is a React and Tailwind CSS data component from Eldora UI, licensed MIT. Copy it and paste it into your project.

Eldora UI MIT data

What it is

Animated Notification List is a React and Tailwind CSS data component from Eldora UI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i react

Licence

This component comes from Eldora UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Animated Notification List

registry/eldoraui/animated-list.tsx
"use client"

import React, { useEffect, useMemo, useState } from "react"
import { AnimatePresence, motion, useAnimationControls } from "motion/react"

import { cn } from "@/lib/utils"

type AnimationPhase = "idle" | "forming_column" | "scrolling_down" | "resetting"

type AnimatedListProps = {
  children: React.ReactNode
  className?: string
  stackGap?: number
  columnGap?: number
  scaleFactor?: number
  scrollDownDuration?: number
  formationDuration?: number
}

type AnimatedListItemProps = {
  children: React.ReactNode
  className?: string
  index: number
  listLength: number
  stackGap?: number
  columnGap?: number
  scaleFactor?: number
}

function InternalAnimatedListItem({
  children,
  className,
  index,
  listLength,
  animationPhase,
  onFormationComplete,
  stackGap = 10,
  columnGap = 100,
  scaleFactor = 0.1,
  formationDuration = 1,
  visibleItemsCount = 4,
  resetSpringStiffness = 120,
  resetSpringDamping = 20,
}: AnimatedListItemProps & {
  animationPhase: AnimationPhase
  onFormationComplete?: () => void
  formationDuration: number
  visibleItemsCount: number
  resetSpringStiffness: number
  resetSpringDamping: number
}) {
  const reverseIndex = listLength - 1 - index
  const isVisible = reverseIndex < visibleItemsCount
  const lastItemOffset = (listLength - 1) * columnGap
  const isLastItem = index === listLength - 1

  const itemVariants = {
    initial: {
      scale: 1 + index * scaleFactor,
      y: reverseIndex * stackGap,
      opacity: isVisible ? 1 : 0,
    },
    column: {
      scale: 1,
      y: index * columnGap - lastItemOffset,
      opacity: 1,
    },
  }

  const target =
    animationPhase === "idle" || animationPhase === "resetting"
      ? "initial"
      : "column"

  const getTransition = () => {
    if (animationPhase === "resetting") {
      return {
        type: "spring" as const,
        stiffness: resetSpringStiffness,
        damping: resetSpringDamping,
      }
    } else {
      return { duration: formationDuration, ease: [0.4, 0, 0.2, 1] as const }
    }
  }

  const handleAnimationComplete = (definition: string) => {
    if (
      isLastItem &&
      definition === "column" &&
      animationPhase === "forming_column"
    ) {
      onFormationComplete?.()
    }
  }

  return (
    <motion.div
      key={index}
      className={cn("absolute inset-x-0 flex w-full justify-center", className)}
      variants={itemVariants}
      initial="initial"
      animate={target}
      transition={getTransition()}
      onAnimationComplete={handleAnimationComplete}
    >
      {children}
    </motion.div>
  )
}

export function AnimatedList({
  children,
  className,
  stackGap = 20,
  columnGap = 85,
  scaleFactor = 0.05,
  scrollDownDuration = 5,
  formationDuration = 1,
}: AnimatedListProps) {
  const initialDelayValue = 500
  const loopPauseDurationValue = 100
  const listResetSpringStiffness = 100
  const listResetSpringDamping = 25
  const itemResetSpringStiffness = 120
  const itemResetSpringDamping = 20
  const visibleItemsCountValue = 4

  const [animationPhase, setAnimationPhase] = useState<AnimationPhase>("idle")
  const listControls = useAnimationControls()
  const childrenArray = useMemo(
    () => React.Children.toArray(children),
    [children]
  )
  const listLength = childrenArray.length
  const totalHeight = listLength * columnGap

  useEffect(() => {
    let timer: NodeJS.Timeout
    if (animationPhase === "idle") {
      timer = setTimeout(
        () => {
          setAnimationPhase("forming_column")
        },
        animationPhase === "idle" ? loopPauseDurationValue : initialDelayValue
      )
    }
    return () => clearTimeout(timer)
  }, [animationPhase, loopPauseDurationValue, initialDelayValue])

  const handleFormationComplete = () => {
    if (animationPhase === "forming_column") setAnimationPhase("scrolling_down")
  }
  const handleScrollDownComplete = () => {
    if (animationPhase === "scrolling_down") setAnimationPhase("resetting")
  }
  const handleScrollUpComplete = () => {
    if (animationPhase === "resetting") setAnimationPhase("idle")
  }

  useEffect(() => {
    if (animationPhase === "scrolling_down") {
      listControls.start({
        y: totalHeight,
        transition: {
          duration: scrollDownDuration,
          ease: [0.4, 0, 0.2, 1] as const,
        },
      })
    } else if (animationPhase === "resetting") {
      listControls.start({
        y: 0,
        transition: {
          type: "spring" as const,
          stiffness: listResetSpringStiffness,
          damping: listResetSpringDamping,
        },
      })
    } else {
      listControls.set({ y: 0 })
    }
  }, [
    animationPhase,
    listControls,
    totalHeight,
    scrollDownDuration,
    listResetSpringStiffness,
    listResetSpringDamping,
  ])

  const handleListAnimationComplete = (definition: { y?: number }) => {
    if (definition.y === totalHeight && animationPhase === "scrolling_down") {
      handleScrollDownComplete()
    } else if (definition.y === 0 && animationPhase === "resetting") {
      handleScrollUpComplete()
    }
  }

  return (
    <motion.div
      className={cn("relative flex h-full w-full items-center", className)}
      initial={{ y: 0 }}
      animate={listControls}
      onAnimationComplete={handleListAnimationComplete}
    >
      <AnimatePresence>
        {childrenArray.map((child, index) => (
          <InternalAnimatedListItem
            key={index}
            index={index}
            listLength={listLength}
            animationPhase={animationPhase}
            onFormationComplete={
              index === listLength - 1 ? handleFormationComplete : undefined
            }
            stackGap={stackGap}
            columnGap={columnGap}
            scaleFactor={scaleFactor}
            formationDuration={formationDuration}
            visibleItemsCount={visibleItemsCountValue}
            resetSpringStiffness={itemResetSpringStiffness}
            resetSpringDamping={itemResetSpringDamping}
          >
            {child}
          </InternalAnimatedListItem>
        ))}
      </AnimatePresence>
    </motion.div>
  )
}

Similar components