# Lattice Loader

An agent status row: a small lattice of cells brightening in a diagonal wave, the current task, and a live stopwatch. Pure CSS animation.

**Category:** Feedback  
**Exports:** `LatticeLoader`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/lattice-loader.json
```
The file lands at `components/ui/lattice-loader.tsx`; import from `@/components/ui/lattice-loader`.

## API — Lattice Loader
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `label` | `string` | — |  |
| `cells` | `number` | `4` |  |
| `running` | `boolean` | `true` |  |

## Example — Lattice Loader

```tsx
import { LatticeLoader } from "@/components/ui/lattice-loader";

export default function LatticeLoaderDemo({ running = true, cells = 4 }: { running?: boolean; cells?: number }) {
  return (
    <div className="grid gap-3">
      <LatticeLoader running={running} cells={cells} label="Indexing repository" />
      <LatticeLoader running={running} cells={cells} label="Embedding 1,204 files" />
    </div>
  );
}
```

## Source — components/ui/lattice-loader.tsx

```tsx
"use client";

import * as React from "react";
import { useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type LatticeLoaderProps = {
  label?: string;
  /** Grid size, cells per side. */
  cells?: number;
  running?: boolean;
  className?: string;
};

const css = `@keyframes charcoal-lattice{0%,100%{opacity:.18;transform:scale(.8)}40%{opacity:1;transform:scale(1)}}`;

/** An agent status row: a small lattice whose cells brighten in a diagonal wave, the current task, and a live stopwatch. CSS animation, so it costs nothing to run. */
export function LatticeLoader({ label = "Indexing repository", cells = 4, running = true, className }: LatticeLoaderProps) {
  const reduce = useReducedMotion();
  const [ms, setMs] = React.useState(0);
  React.useEffect(() => {
    if (!running) return;
    const start = performance.now() - ms;
    const t = setInterval(() => setMs(performance.now() - start), 47);
    return () => clearInterval(t);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [running]);
  const s = ms / 1000;

  return (
    <div role="status" className={cn("inline-flex items-center gap-3 rounded-xl border bg-card px-3 py-2 text-sm text-card-foreground", className)}>
      <style href="charcoal-lattice" precedence="default">
        {css}
      </style>
      <span aria-hidden className="grid gap-[2px]" style={{ gridTemplateColumns: `repeat(${cells}, 4px)` }}>
        {Array.from({ length: cells * cells }, (_, i) => {
          const r = Math.floor(i / cells), c = i % cells;
          return <span key={i} className="size-1 rounded-[1px] bg-ember" style={running && !reduce ? { animation: `charcoal-lattice 1.4s ease-in-out ${(r + c) * 0.1}s infinite` } : { opacity: running ? 0.6 : 1 }} />;
        })}
      </span>
      <span className={running ? "" : "text-muted-foreground"}>{running ? label : `${label} · done`}</span>
      <span className="font-mono text-xs tabular-nums text-muted-foreground">
        {Math.floor(s / 60)}:{String(Math.floor(s % 60)).padStart(2, "0")}.{String(Math.floor((s % 1) * 10))}
      </span>
    </div>
  );
}
```