No description
Find a file
autocommit 5b83c294ab
Some checks failed
Publish / publish (push) Failing after 0s
deps-upgrade(dependencies): ⬆️ Update all dependencies to latest compatible versions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-11 01:24:49 -07:00
.forgejo/workflows chore: initial package split from monorepo 2026-04-20 01:10:59 -07:00
src chore: initial package split from monorepo 2026-04-20 01:10:59 -07:00
.gitignore chore: add .gitignore, remove node_modules/dist/.turbo from tracking 2026-04-20 01:12:54 -07:00
eslint.config.js chore: initial package split from monorepo 2026-04-20 01:10:59 -07:00
package.json deps-upgrade(dependencies): ⬆️ Update all dependencies to latest compatible versions 2026-06-11 01:24:49 -07:00
README.md chore: initial package split from monorepo 2026-04-20 01:10:59 -07:00
tsconfig.json chore: initial package split from monorepo 2026-04-20 01:10:59 -07:00
tsconfig.tsbuildinfo chore: initial package split from monorepo 2026-04-20 01:10:59 -07:00

@lilith/ui-motion

Animation utilities and hooks for framer-motion. Provides reusable patterns for count-up, parallax, staggered, and floating animations.

Features

  • useCountUp - animated number counting with scroll trigger
  • useMultiLayerParallax - multi-layer parallax scrolling effects
  • useStaggeredAnimation - staggered entrance animations for lists
  • useFloatingAnimation - continuous floating/bobbing animations
  • parseStatValue - utility for parsing stat values from strings

Installation

pnpm add @lilith/ui-motion

Peer Dependencies

{
  "react": "^18.0.0",
  "react-dom": "^18.0.0",
  "framer-motion": "^11.0.0"
}

Usage

useCountUp

Animate numbers counting up when element enters viewport:

import { useCountUp } from '@lilith/ui-motion';

function StatCard({ value }: { value: number }) {
  const { value: animatedValue, ref } = useCountUp(value, 2000);

  return (
    <div>
      <span ref={ref}>{animatedValue}</span>
    </div>
  );
}

// Usage
<StatCard value={1500} />
// Displays: 0 → 1 → 15 → 150 → 500 → 1000 → 1500 (animated)

With options:

// Custom duration (3 seconds)
const { value, ref } = useCountUp(1000, 3000);

// Start immediately (don't wait for viewport)
const { value, ref } = useCountUp(1000, 2000, false);

useMultiLayerParallax

Create parallax scrolling effects with multiple layers:

import { useMultiLayerParallax } from '@lilith/ui-motion';
import { motion } from 'framer-motion';

function ParallaxScene() {
  const { containerRef, layers } = useMultiLayerParallax({
    layers: [
      { speed: 0.1 },  // Background - slow
      { speed: 0.3 },  // Midground
      { speed: 0.6 },  // Foreground - fast
    ],
  });

  return (
    <div ref={containerRef} style={{ height: '100vh', overflow: 'hidden' }}>
      <motion.div style={{ y: layers[0].y }}>
        <img src="/mountains.jpg" alt="Background" />
      </motion.div>
      <motion.div style={{ y: layers[1].y }}>
        <img src="/trees.jpg" alt="Midground" />
      </motion.div>
      <motion.div style={{ y: layers[2].y }}>
        <img src="/character.jpg" alt="Foreground" />
      </motion.div>
    </div>
  );
}

useStaggeredAnimation

Stagger animations for list items:

import { useStaggeredAnimation } from '@lilith/ui-motion';
import { motion } from 'framer-motion';

function AnimatedList({ items }: { items: string[] }) {
  const { containerVariants, itemVariants } = useStaggeredAnimation({
    staggerDelay: 0.1,
    duration: 0.4,
  });

  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {items.map((item) => (
        <motion.li key={item} variants={itemVariants}>
          {item}
        </motion.li>
      ))}
    </motion.ul>
  );
}

Custom animation variants:

const { containerVariants, itemVariants } = useStaggeredAnimation({
  staggerDelay: 0.15,
  duration: 0.5,
  hidden: { opacity: 0, x: -20 },
  visible: { opacity: 1, x: 0 },
});

useFloatingAnimation

Create continuous floating/bobbing animations:

import { useFloatingAnimation } from '@lilith/ui-motion';
import { motion } from 'framer-motion';

function FloatingElement({ index }: { index: number }) {
  const floatingProps = useFloatingAnimation(index, 20, 6);

  return (
    <motion.div animate={floatingProps}>
      Floating element {index}
    </motion.div>
  );
}

// Multiple elements with staggered floating
function FloatingCloud() {
  return (
    <div>
      <FloatingElement index={0} />
      <FloatingElement index={1} />
      <FloatingElement index={2} />
    </div>
  );
}

With custom parameters:

// Smaller amplitude, faster animation
const floatingProps = useFloatingAnimation(0, 10, 3);

// Larger amplitude, slower animation
const floatingProps = useFloatingAnimation(0, 40, 10);

parseStatValue

Parse stat values from formatted strings:

import { parseStatValue } from '@lilith/ui-motion';

parseStatValue('1.5K');    // 1500
parseStatValue('2.3M');    // 2300000
parseStatValue('$99.99');  // 99.99
parseStatValue('42');      // 42
parseStatValue('N/A');     // 0

API Reference

useCountUp

function useCountUp(
  end: number,
  duration?: number,     // Default: 2000ms
  startOnView?: boolean  // Default: true
): {
  value: number;
  ref: React.RefObject<HTMLSpanElement>;
}
Parameter Type Default Description
end number required Target number
duration number 2000 Animation duration (ms)
startOnView boolean true Wait for element to be visible

useMultiLayerParallax

function useMultiLayerParallax(config: {
  layers: Array<{ speed: number }>;
}): MultiLayerParallaxReturn;

interface MultiLayerParallaxReturn {
  containerRef: React.RefObject<HTMLDivElement>;
  layers: Array<{
    y: MotionValue<number>;
    speed: number;
  }>;
}

useStaggeredAnimation

function useStaggeredAnimation(config?: {
  staggerDelay?: number;  // Default: 0.1
  duration?: number;      // Default: 0.4
  hidden?: TargetAndTransition;
  visible?: TargetAndTransition;
}): StaggeredAnimationVariants;

interface StaggeredAnimationVariants {
  containerVariants: Variants;
  itemVariants: Variants;
}

useFloatingAnimation

function useFloatingAnimation(
  index?: number,     // Default: 0
  amplitude?: number, // Default: 20
  duration?: number   // Default: 6
): FloatingAnimationProps;

type FloatingAnimationProps = {
  y: number[];
  x: number[];
  rotate: number[];
  transition: {
    duration: number;
    repeat: number;
    ease: string;
    delay: number;
  };
};

Types

import type {
  MultiLayerParallaxReturn,
  StaggeredAnimationVariants,
  FloatingAnimationProps,
} from '@lilith/ui-motion';

Integration with framer-motion

All hooks are designed to work seamlessly with framer-motion's motion components:

import { motion } from 'framer-motion';
import { useCountUp, useFloatingAnimation, useStaggeredAnimation } from '@lilith/ui-motion';

// Count-up with motion
function AnimatedStat({ value }: { value: number }) {
  const { value: count, ref } = useCountUp(value);
  return (
    <motion.div
      initial={{ opacity: 0, scale: 0.5 }}
      animate={{ opacity: 1, scale: 1 }}
    >
      <span ref={ref}>{count}</span>
    </motion.div>
  );
}

// Floating with motion
function Floater({ index }: { index: number }) {
  const floating = useFloatingAnimation(index);
  return <motion.div animate={floating}>Float</motion.div>;
}

// Staggered list with motion
function List({ items }: { items: string[] }) {
  const { containerVariants, itemVariants } = useStaggeredAnimation();
  return (
    <motion.ul variants={containerVariants} initial="hidden" animate="visible">
      {items.map((item) => (
        <motion.li key={item} variants={itemVariants}>{item}</motion.li>
      ))}
    </motion.ul>
  );
}

License

MIT