# Gravity Text Swap

The GravityTextSwap component drops characters into place under a gravity-like fall, so one word gives way to the next instead of simply being replaced.

- **Category:** Text
- **Docs:** https://uibeats.com/docs/text/gravity-text-swap
- **Registry:** https://uibeats.com/r/gravity-text-swap.json
- **License:** MIT
- **npm dependencies:** `motion`

## Installation

```bash
npx shadcn@latest add https://uibeats.com/r/gravity-text-swap.json
```

Or install the dependencies and copy the source below:

```bash
npm install motion
```

## When to use

For a headline that cycles through a few words: audiences, use cases, verbs. Keep the words a similar length so the surrounding layout does not jump on every swap.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `textArray` | `string[]` | `-` | Array of strings to animate between |
| `duration` | `number` | `0.5` | Duration of each word's animation (in seconds) |
| `pauseDuration` | `number` | `2` | Duration to pause on each word before transitioning (in seconds) |
| `className` | `string` | `''` | Additional CSS classes for styling |

## Example usage

```tsx
import React from "react";
import GravityTextSwap from "@/components/demo/text/gravity-text-swap";

const GravityTextSwapUsage = () => {
  return (
    <GravityTextSwap
      textArray={["Code", "Build", "Sleep", "Repeat"]}
      duration={0.5}
      pauseDuration={0.8}
      className="text-md mb-4 md:text-lg"
    />
  );
};

export default GravityTextSwapUsage;
```

## Source

Save as `components/ui/gravity-text-swap.tsx`.

```tsx
"use client";
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

interface GravityTextSwapProps {
  textArray: string[];
  duration?: number;
  pauseDuration?: number;
  className?: string;
}

const GravityTextSwap: React.FC<GravityTextSwapProps> = ({
  textArray,
  duration = 0.5,
  pauseDuration = 2,
  className = "",
}) => {
  const [, setCurrentIndex] = useState(0);
  const [currentText, setCurrentText] = useState(textArray[0]);
  const prefersReducedMotion = useReducedMotion();

  useEffect(() => {
    if (textArray.length <= 1) return;

    const intervalId = setInterval(
      () => {
        setCurrentIndex((prevIndex) => {
          const nextIndex = (prevIndex + 1) % textArray.length;
          setCurrentText(textArray[nextIndex] ?? "");
          return nextIndex;
        });
      },
      (duration + pauseDuration) * 1000,
    );

    return () => clearInterval(intervalId);
  }, [textArray, duration, pauseDuration]);

  return (
    <div className={cn("inline-block overflow-hidden", className)}>
      <AnimatePresence mode="wait">
        <motion.span
          key={currentText}
          className="inline-block"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: duration / 2 }}
        >
          {/*
            The phrases still cycle under reduced motion; stopping them would
            hide every phrase after the first, which is content loss rather
            than calm. What goes is the gravity: no per-character drop and no
            stagger, leaving the crossfade on the wrapper above to carry the
            swap.
          */}
          {prefersReducedMotion
            ? currentText
            : (currentText ?? "").split("").map((char, index) => (
                <motion.span
                  key={index}
                  className="inline-block"
                  initial={{ y: -20, opacity: 0 }}
                  animate={{
                    y: 0,
                    opacity: 1,
                    transition: {
                      type: "spring",
                      damping: 12,
                      stiffness: 200,
                      delay: index * 0.03,
                    },
                  }}
                  exit={{
                    y: 20,
                    opacity: 0,
                    transition: {
                      duration: duration / 2,
                      delay: index * 0.02,
                    },
                  }}
                >
                  {char}
                </motion.span>
              ))}
        </motion.span>
      </AnimatePresence>
    </div>
  );
};

export default GravityTextSwap;
```

## Credits

Written by [Michael](https://github.com/lappemic).
