# Terminus — Animated Text Effect for React

> A split-flap departure board — a grid of matte charcoal tiles whose hinged flaps churn through the character ring and cascade diagonally to the message, each flap landing with a gravity overbite and a shadow cast on the lower half. Only changed tiles animate; optional synthesized flap sound.

- Page: https://crucibleui.com/components/text/terminus
- Category: [text](https://crucibleui.com/components/text)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: motion
- npm dependencies: motion

## Install

```bash
npx shadcn@latest add @crucible/terminus
```

Pro install requires adding the token-bearing `@crucible` registry to components.json (issued at https://crucibleui.com/account after purchase).

Import from `@/components/crucible/terminus`.

## Props (`TerminusProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text?` | `string` | — | A single message to display. Ignored when `messages` is provided. |
| `messages?` | `string[]` | — | Messages to cycle through, one after another, like a departure board. |
| `columns?` | `number` | `the longest message line (clamped 6–22)` | Grid width in tiles. |
| `rows?` | `number` | `the tallest wrapped message` | Grid height in tiles. |
| `align?` | `"left" \| "center"` | `"center"` | Horizontal alignment of text within the grid. |
| `flipDuration?` | `number` | `0.08` | Seconds each single flap takes — the master tempo scale. |
| `cascade?` | `number` | `0.028` | Seconds of diagonal stagger added per tile (top-left → bottom-right). |
| `cycleInterval?` | `number` | `3600` | Milliseconds each message holds before cycling to the next. |
| `loop?` | `boolean` | `true` | Restart from the first message after the last. |
| `charset?` | `string` | — | Ordered ring of characters the flaps churn through. |
| `sound?` | `boolean` | `false` | Play a synthesized mechanical flap click on each flip. |
| `volume?` | `number` | `0.3` | Flap-click volume, 0–1. |
| `tileColor?` | `string` | `"#17171a"` | Matte tile color. |
| `glyphColor?` | `string` | `"#f2ede3"` | Glyph color — warm off-white by default. |
| `seamColor?` | `string` | `"#000000"` | Hairline center-seam color. |
| `tileSize?` | `number` | `46` | Tile edge length in px. |
| `gap?` | `number` | `4` | Gap between tiles in px. |
| `radius?` | `number` | `6` | Tile corner radius in px. |
| `paused?` | `boolean` | `false` | Freeze cycling on the current message. |

Also accepts: `Omit<React.ComponentPropsWithoutRef<"div">, "children">`.

## Usage example

```tsx
"use client";

import * as React from "react";

import { Terminus } from "@/components/crucible/terminus";

export default function TerminusDemo() {
  const [sound, setSound] = React.useState(false);

  return (
    <div className="relative isolate flex h-full min-h-[360px] w-full flex-col items-center justify-center gap-8 overflow-hidden rounded-xl bg-neutral-950">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.04),transparent_70%)]"
      />
      <div className="relative z-10 flex flex-col items-center gap-6 px-6 text-center">
        <Terminus
          messages={["NOW BOARDING", "SHIP IT FAST", "ALL SYSTEMS GO"]}
          columns={13}
          tileSize={40}
          sound={sound}
        />
        <div className="flex flex-col items-center gap-3">
          <p className="text-sm text-neutral-400">
            Each tile reflaps only when its letter changes — watch the wave roll diagonally.
          </p>
          <button
            type="button"
            onClick={() => setSound((s) => !s)}
            aria-pressed={sound}
            className="rounded-full border border-white/10 bg-white/5 px-4 py-1.5 text-[11px] font-medium uppercase tracking-[0.18em] text-neutral-300 transition hover:bg-white/10"
          >
            Flap sound: {sound ? "on" : "off"}
          </button>
        </div>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.
