# Dither Motion

> Moves a chart's dithered dots: an intro, a loop at rest, and dots that fill in near the pointer. It draws on a canvas under the chart.

- Name: `dither-motion`
- Status: beta
- Categories: motion, display
- File: the install writes `dither-motion.tsx` to your `ui` alias, `@/components/ui` by default.
- Other registry items: `@arbokui/dither`, `@arbokui/motion-timing`. The install adds them.
- Gallery: https://arbokui.com/components/dither-motion
- Registry item: https://arbokui.com/r/dither-motion.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/dither-motion
   ```

## Use it for

- A chart whose dithered areas should move. Put a canvas under its SVG, the same size, and pass both with the areas' paths and colors.
- Load it with import() on first hover or focus, or once the chart is on screen. The dots at rest are the SVG mask, so nothing waits on it.
- The same box the chart passes to DitherBoxMask, so the canvas and the mask draw the same dots. A chart with a mask per bar passes each bar's box as boxes, and the same thin.

## Avoid it for

- Dots that never move. DitherBoxMask draws them with no script.
- Shapes other than filled areas. Lines and labels stay in the SVG, on top.

## Props

- `startDitherMotion`: `(options: DitherMotionOptions) => DitherMotion`. Helper. Sets up moving dots for one chart and returns intro, idle, point, and stop. Options: host, the chart's root; canvas; width and height, the SVG's size in CSS pixels; box; boxes, one per bar, each laying out its own dots; fill; start; thin; and shapes, each { d, paint, top?, bottom?, fade? }, bottom area first. Nothing draws until one of them asks.
- `intro`: `(style: "sweep" | "rain" | "ripple" | "pop" | "fall") => Promise<void>`. Plays an intro over the motion pulse time. It resolves when the last dot is in, or at once when motion is off.
- `idle`: `(style: "none" | "wave" | "twinkle" | "breathe" | "drift") => void`. A loop at rest. Wave passes a band of extra dots across, then rests. Twinkle blinks single dots out and on, each on its own clock. Breathe fills the thin edge in deeper and back. Drift moves soft patches of extra dots slowly across. It stops while the chart is off screen.
- `point`: `(x: number | null, y?: number) => void`. Fills in hidden dots within 56px of the pointer, in the SVG's pixels. Pass null when the pointer leaves, and they fade back.
- `stop`: `() => void`. Stops every motion, clears the canvas, and hands the dots back to the mask. Call it on unmount.

## Accessibility

- Motion off and reduced motion set the pulse to 0, and then nothing moves. The mask keeps drawing the dots at rest.
- Forced colors and texture flat move nothing either, since the chart draws no dots then.
- The canvas is aria-hidden. The chart's SVG keeps its name, and the dots carry no meaning of their own.

## Examples

### Play an intro on a dithered area

```tsx
import { useEffect, useRef } from "react";

function MovingArea({ d, box }: { d: string; box: DitherBox }) {
  const host = useRef<HTMLDivElement>(null);
  const canvas = useRef<HTMLCanvasElement>(null);
  useEffect(() => {
    if (!host.current || !canvas.current) return;
    const motion = startDitherMotion({ host: host.current, canvas: canvas.current, width: 320, height: 160, box, shapes: [{ d, paint: "var(--chart-1)" }] });
    void motion.intro("sweep");
    return () => motion.stop();
  }, [d, box]);
  return (
    <Dither>
      <div ref={host} className="relative h-40 w-80">
        <canvas ref={canvas} aria-hidden className="absolute inset-0 size-full" />
        <svg width={320} height={160} className="absolute inset-0" aria-hidden>
          <defs>
            <DitherBoxMask id="area-dots" box={box} />
          </defs>
          <path d={d} fill="var(--chart-1)" mask="url(#area-dots)" />
        </svg>
      </div>
    </Dither>
  );
}
```
