# Matrix Hero (`matrix-hero`)

> Full-width hero section with cascading matrix rain and centered green-phosphor glow text.

- **Docs:** https://www.mellowui.com/components/matrix-hero
- **Markdown:** https://www.mellowui.com/components/matrix-hero.md
- **Registry:** https://www.mellowui.com/r/matrix-hero.json
- **Tool prompt:** https://www.mellowui.com/api/prompt/matrix-hero
- **Categories:** display, canvas, animation
- **Dependencies:** none

## AI prompt

Add a MatrixHero component from the mellow library. It renders a full-width hero section with cascading half-width katakana matrix rain as the background and centered green-phosphor glow text on top. Pass text='...' for a single headline, or children for custom content. Key props: text, speed, paused, charset, glowIntensity.

## Install

```bash
npx shadcn@latest add https://mellowui.com/r/matrix-hero.json
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `text` | `string` | `"Welcome to the Matrix, Neo."` | Hero headline displayed centered over the rain. Ignored when children is provided. |
| `speed` | `number` | `1` | Rain fall speed multiplier. |
| `paused` | `boolean` | `false` | Freeze the rain animation. |
| `charset` | `string` | — | Characters used in the rain columns. Defaults to half-width katakana + digits. |
| `glowIntensity` | `number` | `1` | Multiplier for the text glow radius. 0 disables glow. |
| `className` | `string` | — | Additional CSS classes for the outer wrapper. |
| `children` | `React.ReactNode` | — | Custom content rendered centered over the rain, replacing the default text. |

## Design notes

- Client component (`"use client"`) — required for animation and refs.
- Uses Mellow design tokens: `--ink`, `--ink-rgb`, `--background`, `--background-rgb`, `--rule`, `--font-sans`, `--font-serif`, `--font-mono`. Tokens auto-flip between light and dark themes — never hardcode colors.
- Respects `prefers-reduced-motion: reduce` where applicable.
- Self-contained — no shared utilities. Copy-paste, not a package.

## Source

```tsx
"use client";

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

const DEFAULT_CHARSET =
  "ｦｧｨｩｪｫｬｭｮｯｱｲｳｴｵｶｷｸｹｺｻｼｽｾｿﾀﾁﾂﾃﾄﾅﾆﾇﾈﾉﾊﾋﾌﾍﾎﾏﾐﾑﾒﾓﾔﾕﾖﾗﾘﾙﾚﾛﾜﾝ01234567";

const TRAIL = 10;
const FONT_SIZE = 14;

const CHAR_DELAY = 60;
const LINE_PAUSE = 150;
const BLINK_MS  = 530;

function parseLines(text: string | string[]): string[] {
  if (Array.isArray(text)) return text.filter(Boolean);
  return text.split("\n").filter(Boolean);
}

export interface MatrixHeroProps {
  text?: string | string[];
  speed?: number;
  paused?: boolean;
  charset?: string;
  glowIntensity?: number;
  className?: string;
  style?: React.CSSProperties;
  children?: React.ReactNode;
}

export function MatrixHero({
  text = ["Welcome to the Matrix,", "Neo."],
  speed = 1,
  paused = false,
  charset = DEFAULT_CHARSET,
  glowIntensity = 1,
  className,
  style,
  children,
}: MatrixHeroProps) {
  useEffect(() => {
    if (document.getElementById("vt323-font")) return;
    const link = document.createElement("link");
    link.id = "vt323-font";
    link.rel = "stylesheet";
    link.href = "https://fonts.googleapis.com/css2?family=VT323&display=swap";
    document.head.appendChild(link);
  }, []);

  const canvasRef = useRef<HTMLCanvasElement>(null);
  const sizeRef   = useRef({ w: 0, h: 0 });
  const rainRef   = useRef<{ head: number; spd: number }[]>([]);
  const frameRef  = useRef(0);
  const pausedRef = useRef(paused);
  useEffect(() => { pausedRef.current = paused; }, [paused]);

  useEffect(() => {
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (!canvasRef.current) return;
    const canvas: HTMLCanvasElement = canvasRef.current;
    const ctxOrNull = canvas.getContext("2d");
    if (!ctxOrNull) return;
    const ctx: CanvasRenderingContext2D = ctxOrNull;

    function resize() {
      const dpr = Math.min(window.devicePixelRatio, 2);
      const rect = canvas.getBoundingClientRect();
      sizeRef.current = { w: rect.width, h: rect.height };
      canvas.width  = rect.width  * dpr;
      canvas.height = rect.height * dpr;
      ctx.scale(dpr, dpr);
    }

    const ro = new ResizeObserver(resize);
    ro.observe(canvas);
    resize();

    function draw() {
      const { w, h } = sizeRef.current;
      if (w === 0 || h === 0) { frameRef.current = requestAnimationFrame(draw); return; }

      ctx.font = `${FONT_SIZE}px monospace`;
      const charW = ctx.measureText("M").width;
      const charH = FONT_SIZE * 1.2;
      const cols  = Math.floor(w / charW);
      const rows  = Math.floor(h / charH);

      if (rainRef.current.length !== cols) {
        rainRef.current = Array.from({ length: cols }, (_, ci) => ({
          head: (Math.sin(ci * 7.3) * 0.5 + 0.5) * rows,
          spd:  0.08 + ((ci * 13) % 17) / 17 * 0.25,
        }));
      }

      if (!reduced && !pausedRef.current) {
        ctx.fillStyle = "#000000";
        ctx.fillRect(0, 0, w, h);
        ctx.font = `${FONT_SIZE}px monospace`;
        ctx.textBaseline = "top";

        for (let ci = 0; ci < cols; ci++) {
          const r = rainRef.current[ci];
          if (!r) continue;
          r.head += r.spd * speed;
          if (r.head - TRAIL > rows) {
            r.head = -(Math.random() * rows * 0.3);
            r.spd  = 0.08 + Math.random() * 0.25;
          }
        }

        for (let ci = 0; ci < cols; ci++) {
          const rain = rainRef.current[ci];
          if (!rain) continue;
          for (let ri = 0; ri < rows; ri++) {
            const dist = rain.head - ri;
            if (dist < 0 || dist > TRAIL) continue;
            const t = 1 - dist / TRAIL;
            const isHead = dist < 1.2;
            const chIdx =
              Math.abs(ci * 31 + ri * 17 + Math.floor(rain.head * 2.5)) %
              charset.length;
            const ch = charset[chIdx] ?? "0";
            ctx.fillStyle = isHead
              ? `rgba(190,255,190,${(t * 0.85).toFixed(2)})`
              : `rgba(0,${Math.floor(55 + t * 130)},0,${(t * 0.5).toFixed(2)})`;
            ctx.fillText(ch, ci * charW, ri * charH);
          }
        }
      }

      frameRef.current = requestAnimationFrame(draw);
    }

    frameRef.current = requestAnimationFrame(draw);
    return () => { cancelAnimationFrame(frameRef.current); ro.disconnect(); };
  }, [speed, charset]);

  const lines = parseLines(Array.isArray(text) ? text : (text ?? ""));
  const [displayed, setDisplayed] = useState<string[]>(() => lines.map(() => ""));
  const [activeLine, setActiveLine]   = useState(0);
  const [typingDone, setTypingDone]   = useState(false);
  const [cursorOn, setCursorOn]       = useState(true);

  const textKey = typeof text === "string" ? text : JSON.stringify(text);

  useEffect(() => {
    const ls = parseLines(Array.isArray(text) ? text : (text ?? ""));
    setDisplayed(ls.map(() => ""));
    setActiveLine(0);
    setTypingDone(false);
    setCursorOn(true);

    let lineIdx  = 0;
    let charIdx  = 0;
    let timer: ReturnType<typeof setTimeout>;

    function typeNext() {
      if (pausedRef.current) { timer = setTimeout(typeNext, 50); return; }

      const target = ls[lineIdx];
      if (!target) { setTypingDone(true); return; }

      if (charIdx < target.length) {
        charIdx++;
        const snap = charIdx;
        const snap_l = lineIdx;
        setDisplayed(prev => {
          const next = [...prev];
          next[snap_l] = target.slice(0, snap);
          return next;
        });
        timer = setTimeout(typeNext, CHAR_DELAY);
      } else if (lineIdx < ls.length - 1) {
        lineIdx++;
        charIdx = 0;
        setActiveLine(lineIdx);
        timer = setTimeout(typeNext, LINE_PAUSE);
      } else {
        setTypingDone(true);
      }
    }

    timer = setTimeout(typeNext, CHAR_DELAY);
    return () => clearTimeout(timer);
  }, [textKey]);

  useEffect(() => {
    const id = setInterval(() => setCursorOn(v => !v), BLINK_MS);
    return () => clearInterval(id);
  }, []);

  const g = glowIntensity;
  const textShadow =
    g > 0
      ? `0 0 ${7 * g}px #00FF41, 0 0 ${16 * g}px #00FF41, 0 0 ${32 * g}px rgba(0,255,65,0.45)`
      : "none";

  return (
    <div
      className={className}
      style={{
        position: "relative",
        width: "100%",
        height: "100%",
        background: "#000",
        overflow: "hidden",
        ...style,
      }}
    >
      <canvas
        ref={canvasRef}
        style={{ position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }}
      />
      <div
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          zIndex: 1,
          padding: "2rem",
          pointerEvents: "none",
        }}
      >
        {children ?? (
          <div style={{ textAlign: "center" }}>
            {lines.map((_, i) => {
              const showCursor = i === activeLine && !typingDone
                ? true
                : i === lines.length - 1 && typingDone;
              return (
                <div
                  key={i}
                  style={{
                    fontFamily: "'VT323', monospace",
                    fontSize: "clamp(1.1rem, 2.8vw, 2.25rem)",
                    fontWeight: 700,
                    color: "#00FF41",
                    textShadow,
                    lineHeight: 1.4,
                    letterSpacing: "0.04em",
                    whiteSpace: "pre",
                    minHeight: "1.4em",
                  }}
                >
                  {displayed[i] ?? ""}
                  {showCursor && (
                    <span style={{
                      opacity: (i === activeLine && !typingDone) ? 1 : (cursorOn ? 1 : 0),
                      display: "inline-block",
                      transform: "scaleY(1.5) translateY(-0.1em)",
                      transformOrigin: "center",
                    }}>
                      ▮
                    </span>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

export default MatrixHero;

```

## Demo

```tsx
"use client";

import MatrixHero from "@/registry/mellow/matrix-hero";

export default function MatrixHeroDemo() {
  return (
    <div style={{ width: "100%", height: "100%" }}>
      <MatrixHero />
    </div>
  );
}

```
