# Misfire — Animated Section Block for React

> A full-width 404 section in three variants — a knock-over-able physics "404" pile (Scree), a sonar scope behind a "Signal lost" readout (Sonar), and a split-flap board that keeps trying the route and lands on NOT FOUND (Terminus). Real h1, labelled search form, home link, suggestion chips; settled frames under reduced motion.

- Page: https://crucibleui.com/components/sections/misfire
- Category: [sections](https://crucibleui.com/components/sections)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: canvas
- npm dependencies: motion, matter-js

## Install

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

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

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

## Props (`MisfireProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant?` | `MisfireVariant` | `"pile"` | Visual treatment: - `pile` — a "404" whose digits are physics bodies you can knock over and fling. - `signal` — a sonar scope behind a "Signal lost" readout with a blinking status. - `board` — a split-flap board that keeps trying the path and lands on NOT FOUND. |
| `path?` | `string` | `"/old-page"` | The requested path that could not be found — shown in the copy/visual. |
| `homeHref?` | `string` | `"/"` | Primary "Back home" link target. |
| `homeLabel?` | `string` | `"Back home"` | Label of the home link. |
| `onSearch?` | `(query: string) => void` | — | Called with the trimmed query when the search form is submitted. When omitted the form submits natively (GET `?q=` to `homeHref`). |
| `searchPlaceholder?` | `string` | `"Search the site"` | Placeholder of the search field. |
| `suggestions?` | `MisfireSuggestion[]` | `[]` | Link chips offered as likely destinations. |
| `title?` | `string` | `"Page not found"` | Real h1 text — always present for assistive tech and SEO. |
| `description?` | `string` | `per-variant line` | Short copy under the heading. |
| `accentColor?` | `string` | `"#7dd3fc"` | The single accent hue (pile highlight digit, scope phosphor, status lamp). |
| `cycle?` | `number` | `4000` | Board variant: ms for one full try-and-fail cycle. |
| `paused?` | `boolean` | `false` | Freeze every animated piece at its settled frame. |

Also accepts: `Omit<React.ComponentPropsWithoutRef<"section">, "title" | "onSubmit">`.

## Usage example

```tsx
"use client";

import * as React from "react";

import { Misfire, type MisfireVariant } from "@/components/crucible/misfire";

const VARIANTS: { id: MisfireVariant; label: string }[] = [
  { id: "pile", label: "Pile" },
  { id: "signal", label: "Signal" },
  { id: "board", label: "Board" },
];

const SUGGESTIONS = [
  { label: "Getting started", href: "#start" },
  { label: "Components", href: "#components" },
  { label: "Pricing", href: "#pricing" },
];

export default function MisfireDemo() {
  const [variant, setVariant] = React.useState<MisfireVariant>("pile");
  const tabRefs = React.useRef<(HTMLButtonElement | null)[]>([]);
  const uid = React.useId();

  const onKeyDown = (e: React.KeyboardEvent, i: number) => {
    let next = i;
    if (e.key === "ArrowRight") next = (i + 1) % VARIANTS.length;
    else if (e.key === "ArrowLeft") next = (i - 1 + VARIANTS.length) % VARIANTS.length;
    else if (e.key === "Home") next = 0;
    else if (e.key === "End") next = VARIANTS.length - 1;
    else return;
    e.preventDefault();
    setVariant(VARIANTS[next]!.id);
    tabRefs.current[next]?.focus();
  };

  return (
    <div className="relative isolate flex h-[520px] min-h-[360px] w-full items-center justify-center overflow-hidden rounded-xl bg-neutral-950">
      <div className="relative z-10 flex h-full w-full flex-col">
        <div className="flex flex-col items-center gap-2 pt-4">
          <div
            role="tablist"
            aria-label="404 variant"
            className="inline-flex rounded-full border border-white/10 bg-white/[0.03] p-1"
          >
            {VARIANTS.map((v, i) => {
              const selected = v.id === variant;
              return (
                <button
                  key={v.id}
                  ref={(el) => {
                    tabRefs.current[i] = el;
                  }}
                  type="button"
                  role="tab"
                  id={`${uid}-tab-${v.id}`}
                  aria-selected={selected}
                  aria-controls={`${uid}-panel`}
                  tabIndex={selected ? 0 : -1}
                  onClick={() => setVariant(v.id)}
                  onKeyDown={(e) => onKeyDown(e, i)}
                  className={
                    "rounded-full px-3 py-1 text-xs font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/60 " +
                    (selected ? "bg-white/10 text-neutral-50" : "text-neutral-400 hover:text-neutral-200")
                  }
                >
                  {v.label}
                </button>
              );
            })}
          </div>
          <p className="text-[11px] font-medium uppercase tracking-[0.2em] text-neutral-500">
            Switch variants — try knocking the digits over.
          </p>
        </div>
        <div
          role="tabpanel"
          id={`${uid}-panel`}
          aria-labelledby={`${uid}-tab-${variant}`}
          className="relative flex min-h-0 flex-1"
        >
          <Misfire
            key={variant}
            variant={variant}
            path="/docs/old-page"
            suggestions={SUGGESTIONS}
            onSearch={() => {}}
            className="h-full py-4"
          />
        </div>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through. Pauses offscreen and on hidden tabs, clamps device pixel ratio, and tears down its GL/canvas context on unmount.
