# Terminal Decode Loader

> Terminal-style text decoder that cycles through random characters before resolving

A shadcn/ui block from the [@elements](https://www.tryelements.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/elements/loader-terminal-decode).

- Page, with a live preview: https://shadcn.exchange/r/elements/loader-terminal-decode
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/elements/loader-terminal-decode-light-fa46a0d83ca0287b.png
- Registry item: https://www.tryelements.dev/r/loader-terminal-decode.json
- Categories: loaders
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.tryelements.dev/r/loader-terminal-decode.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Notes from the registry

Cipher-cracking text decoder with configurable charset (symbols, binary, hex), speed, and loop behavior. Monospace terminal aesthetic.

## Files

### `registry/default/blocks/loaders/loader-terminal-decode/components/elements/loader-terminal-decode.tsx`

```tsx
"use client";

import { useCallback, useEffect, useRef, useState } from "react";

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

const CHARSETS = {
  symbols: "!@#$%^&*()_+-=[]{}|;:,.<>?/~",
  binary: "01",
  hex: "0123456789ABCDEF",
};

type DisplayChar = {
  char: string;
  resolved: boolean;
};

export type LoaderTerminalDecodeProps = {
  text?: string;
  speed?: number;
  charset?: "symbols" | "binary" | "hex";
  loop?: boolean;
  className?: string;
};

export function LoaderTerminalDecode({
  text = "LOADING",
  speed = 30,
  charset = "symbols",
  loop = true,
  className,
}: LoaderTerminalDecodeProps) {
  const [displayChars, setDisplayChars] = useState<DisplayChar[]>([]);
  const rafRef = useRef<number | null>(null);
  const startTimeRef = useRef<number>(0);
  const lastResolveTimeRef = useRef<number>(0);
  const resolvedCountRef = useRef<number>(0);
  const isWaitingRef = useRef<boolean>(false);
  const waitStartRef = useRef<number>(0);

  const chars = CHARSETS[charset];

  const getRandomChar = useCallback(
    () => chars[Math.floor(Math.random() * chars.length)],
    [chars],
  );

  const reset = useCallback(() => {
    setDisplayChars(
      text.split("").map((char) => ({
        char: char === " " ? " " : getRandomChar(),
        resolved: char === " ",
      })),
    );
    resolvedCountRef.current = text.split("").filter((c) => c === " ").length;
    lastResolveTimeRef.current = 0;
    isWaitingRef.current = false;
  }, [text, getRandomChar]);

  useEffect(() => {
    reset();
    startTimeRef.current = performance.now();

    const animate = (currentTime: number) => {
      const elapsed = currentTime - startTimeRef.current;

      if (isWaitingRef.current) {
        if (currentTime - waitStartRef.current >= 1500) {
          reset();
          startTimeRef.current = currentTime;
          lastResolveTimeRef.current = 0;
        }
        rafRef.current = requestAnimationFrame(animate);
        return;
      }

      const shouldResolveNext =
        resolvedCountRef.current < text.length &&
        elapsed - lastResolveTimeRef.current >= speed;

      setDisplayChars((prev) =>
        prev.map((item, index) => {
          if (item.resolved) {
            return item;
          }

          if (text[index] === " ") {
            return { char: " ", resolved: true };
          }

          if (shouldResolveNext && index === resolvedCountRef.current) {
            lastResolveTimeRef.current = elapsed;
            resolvedCountRef.current++;
            return { char: text[index], resolved: true };
          }

          return { char: getRandomChar(), resolved: false };
        }),
      );

      if (resolvedCountRef.current >= text.length) {
        if (loop) {
          isWaitingRef.current = true;
          waitStartRef.current = currentTime;
        }
      }

      rafRef.current = requestAnimationFrame(animate);
    };

    rafRef.current = requestAnimationFrame(animate);

    return () => {
      if (rafRef.current !== null) {
        cancelAnimationFrame(rafRef.current);
      }
    };
  }, [text, speed, loop, reset, getRandomChar]);

  return (
    <output
      data-slot="loader-terminal-decode"
      aria-live="polite"
      aria-label={text}
      className={cn("inline-flex font-mono", className)}
    >
      <span className="sr-only">{text}</span>
      <span aria-hidden="true" className="inline-flex">
        {displayChars.map((item, i) => (
          <span
            // biome-ignore lint/suspicious/noArrayIndexKey: stable list derived from fixed text length
            key={i}
            className={cn(
              "inline-block w-[1ch] text-center transition-colors duration-150",
              item.resolved ? "text-foreground" : "text-muted-foreground",
            )}
          >
            {item.char}
          </span>
        ))}
      </span>
    </output>
  );
}
```
