# Kinetic beat

> Words hit on a beat.

- Type: Component (kinetic)
- Page: https://textlab.dev/components/kinetic-beat
- Markdown: https://textlab.dev/components/kinetic-beat.md
- Registry item: https://textlab.dev/r/kinetic-beat.json
- Source file: `registry/textlab/kinetic-beat/kinetic-beat.tsx`
- Dependencies: motion
- Variants: pop, slam, bounce
- Keywords: kinetic typography, beat, bpm, lyric, slogan, music

## When to use

- Music, events and launch teasers.
- Short punchy slogans of three to six words.

## When not to use

- Information people need to read at their own pace.
- Long sentences.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/kinetic-beat
pnpm dlx shadcn@latest add @textlab/kinetic-beat
```

The `@textlab` name needs `"registries": { "@textlab": "https://textlab.dev/r/{name}.json" }` in `components.json`. Without it, use the full URL:

```bash
npx shadcn@latest add https://textlab.dev/r/kinetic-beat.json
```

Import the tokens once in your root layout: `import "@/components/textlab/tokens.css";`

### Manual

1. Install the dependencies: `npm install motion@12`
2. Copy `text-fx` (https://textlab.dev/r/text-fx.json) and this component into `components/textlab/`.

## Usage

```tsx
import { KineticBeat } from "@/components/textlab/kinetic-beat/kinetic-beat";

export function Example() {
  return <KineticBeat text="Make it loud" bpm={128} variant="slam" />;
}
```

## API reference

### KineticBeat

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | Words to hit, one per beat. |
| `variant` | `"pop" \| "slam" \| "bounce"` | `"pop"` | Scale up with overshoot, crash down from large, or hop in. |
| `bpm` | `number` | `120` | Beats per minute. |
| `loop` | `boolean` | `true` | Start over after the last word. |
| `rest` | `number` | `2` | Silent beats at the end of each loop. |
| `as` | `ElementType` | `"div"` | Rendered element. |
| `reducedMotion` | `"user" \| "always" \| "never"` | `"user"` | user follows the OS setting; always shows the still version. |
| `className` | `string` | – | Merged onto the root. |

## Accessibility

- The whole sentence is read once from a hidden copy.
- Pauses when scrolled out of view.

## Motion

- One word per beat; at 120 BPM that is two words a second.
- Exits are quick so each hit lands clean.

## Reduced motion

The sentence appears whole and still.

## Performance

- One interval and one animated word at a time.

## Notes for AI

- Use only the props documented above. Do not invent props.
- Keep the screen-reader text and the reduced-motion branch when editing the source.
- Pick a variant from: pop, slam, bounce.

## Related

- [Word sequence](https://textlab.dev/components/word-sequence.md): A sentence choreographed word by word.
- [Word rotator](https://textlab.dev/components/word-rotator.md): Cycles words while the width follows.
- [Velocity marquee](https://textlab.dev/components/velocity-marquee.md): A marquee that speeds up, slows and reverses with scroll.

## Also in kinetic

- [Read along](https://textlab.dev/components/read-along.md): A highlight follows the reading, word by word.
- [Echo text](https://textlab.dev/components/echo-text.md): Every change leaves fading echoes behind.

## Source

### components/textlab/kinetic-beat/kinetic-beat.tsx

```tsx
"use client";

import { useEffect, useState, type CSSProperties, type ElementType } from "react";
import { AnimatePresence, motion, type TargetAndTransition, type Transition } from "motion/react";
import { srOnly } from "../lib/a11y";
import { splitWords } from "../lib/split";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";
import { useTrigger } from "../lib/use-trigger";

export type KineticBeatVariant = "pop" | "slam" | "bounce";

export interface KineticBeatProps {
  /** Words to hit, one per beat. */
  text: string;
  /** pop scales up with a little overshoot, slam crashes down from large and blurred, bounce hops in. */
  variant?: KineticBeatVariant;
  /** Beats per minute. */
  bpm?: number;
  /** Start over after the last word. */
  loop?: boolean;
  /** Beats of silence at the end of each loop. */
  rest?: number;
  as?: ElementType;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

const MOVES: Record<KineticBeatVariant, { initial: TargetAndTransition; transition: Transition; exit: TargetAndTransition }> = {
  pop: { initial: { scale: 0.4, opacity: 0 }, transition: { type: "spring", visualDuration: 0.22, bounce: 0.3 }, exit: { scale: 1.15, opacity: 0, transition: { duration: 0.12 } } },
  // Slam is an impact: it accelerates into the landing on purpose, so it uses an ease-in curve.
  slam: { initial: { scale: 2.6, opacity: 0, filter: "blur(10px)" }, transition: { duration: 0.16, ease: [0.5, 0, 1, 0.5] }, exit: { opacity: 0, transition: { duration: 0.08 } } },
  bounce: { initial: { y: "-0.8em", opacity: 0 }, transition: { type: "spring", visualDuration: 0.28, bounce: 0.3 }, exit: { y: "0.4em", opacity: 0, transition: { duration: 0.12 } } },
};

/**
 * Words hit on a beat, one per count, like a lyric video. Timing comes from the tempo, so it can be
 * synced to music by setting the same BPM.
 */
export function KineticBeat({ text, variant = "pop", bpm = 120, loop = true, rest = 2, as: Tag = "div", reducedMotion = "user", className, style }: KineticBeatProps) {
  const { ref, on } = useTrigger<HTMLElement>({ trigger: "inView", once: false });
  const reduced = useReduced(reducedMotion);
  const words = splitWords(text).map((w) => w.text);
  const [beat, setBeat] = useState(0);
  const cycle = words.length + rest;

  useEffect(() => {
    if (!on || reduced) return;
    const id = setInterval(() => setBeat((b) => (loop ? (b + 1) % cycle : Math.min(b + 1, words.length - 1))), 60000 / bpm);
    return () => clearInterval(id);
  }, [on, reduced, bpm, loop, cycle, words.length]);

  if (reduced) {
    return (
      <Tag ref={ref} className={className} style={style}>
        {text}
      </Tag>
    );
  }

  const word = beat < words.length ? words[beat] : null;
  const move = MOVES[variant];
  return (
    <Tag ref={ref} className={className} style={{ position: "relative", display: "grid", placeItems: "center", ...style }}>
      <span style={srOnly}>{text}</span>
      {/* The longest word reserves the stage so the layout never jumps between beats. */}
      <span aria-hidden="true" style={{ gridArea: "1 / 1", visibility: "hidden", whiteSpace: "nowrap" }}>
        {words.reduce((a, b) => (b.length > a.length ? b : a), "")}
      </span>
      <AnimatePresence mode="popLayout">
        {word && (
          <motion.span
            key={`${beat}-${word}`}
            aria-hidden="true"
            style={{ gridArea: "1 / 1", whiteSpace: "nowrap", display: "inline-block" }}
            initial={move.initial}
            animate={{ scale: 1, opacity: 1, y: 0, filter: "blur(0px)" }}
            exit={move.exit}
            transition={move.transition}
          >
            {word}
          </motion.span>
        )}
      </AnimatePresence>
    </Tag>
  );
}

export default KineticBeat;
```

Full library index: https://textlab.dev/llms.txt
