# Drawing Icon

> A line icon that draws itself stroke by stroke while the step it stands for is running.

- Category: loaders
- Job: Oversee — Show progress and reasoning while the AI works, and let people approve, pause or stop it.
- Slug: `drawing-icon`
- HTML: https://ai-patterns.app/patterns/loaders/drawing-icon
- JSON: https://ai-patterns.app/api/patterns/loaders/drawing-icon
- Seen in: [chatgpt](https://chatgpt.com)

## Summary
A line icon that draws itself stroke by stroke, holds for a beat, then erases and starts again — used as the "working" glyph for a specific kind of activity (searching, thinking, writing, making an image). Because it's the activity's own icon rather than a generic spinner, it says *what* is happening as well as *that* something is happening.

## When to use
- Next to a status label while an agent runs a step whose kind you know ("Searching the web", "Creating image").
- In tool-call chips, step lists or traces where each step type already has an icon — animate the icon of the step that is running.
- When duration is unknown and you want a calmer, more characterful signal than a spinner.

## When not to use
- When you know real progress — use a progress bar.
- For more than one running step in the same view; several icons drawing at once reads as noise. Animate only the active one and show the rest static.
- With dense, filled or very detailed icons — the effect needs a few clean strokes to read.
- As decoration on idle UI. Motion should mean "working right now".

## Anatomy
- Icon: a 24×24 stroke icon split into its individual paths (2–7 strokes works best), drawn in path order.
- Optional ghost: a faint, static copy of the full icon underneath, so the shape is recognisable at every frame, including the empty one.
- Label (outside the component): the present-participle verb for the step.

## Behavior
- Each stroke animates its dash offset: draw on (~45% of the loop), hold fully drawn (~20%), then slide off the far end (~35%), easing in and out. Default loop is 3.6s.
- Strokes start staggered (default 0.3s apart), so the icon builds up in sequence rather than all at once.
- Works for any path: every stroke is normalised with `pathLength="1"`, so one keyframe set fits every shape.
- When the step changes, swap the icon (remount it so it starts drawing from empty) and the label together.
- When the step finishes, pause it: it settles on the fully drawn icon, which then becomes the step's static "done" glyph.

## Content guidelines
- Pick the icon that matches the actual activity; don't show a globe unless something is actually being searched.
- Pair it with a short label — the icon alone is a hint, not a status.
- Order paths so the drawing reads naturally: outline first, details last.

## Accessibility
- The icon is decorative (`aria-hidden`) when a visible label sits next to it; otherwise pass a `label` so it's announced as an image.
- Put the status label in an `aria-live="polite"` region so step changes are announced.
- With `prefers-reduced-motion`, the icon is shown fully drawn and only gently pulses its opacity.

## Related patterns
- **Thinking Loader** — Use it as well, replacing its static glyph with a Drawing Icon for the step that is running.
- **Tool Call Chip** — Use it as well to animate the chip's icon while the call runs.
- **Analysis List** — Use it as well to animate only the active step's icon.

## Implementation

Reference implementation (React, TypeScript, Tailwind v4, Motion, Radix). Keep every rule from the spec above; if the host project has its own design system, re-express the markup and motion in it rather than copying these classes.

Local imports to provide: `@/lib/utils` (`cn` is the usual `clsx` + `tailwind-merge` helper).

```tsx title="component.tsx"
"use client";

import * as React from "react";

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

/**
 * Stroke paths for a few 24×24 line icons (Lucide, ISC). Circles are written
 * as two arcs so every shape is a `<path>` that can be "drawn".
 */
export const DRAWING_ICONS = {
  origami: [
    "M12 12V4a1 1 0 0 1 1-1h6.297a1 1 0 0 1 .651 1.759l-4.696 4.025",
    "m12 21-7.414-7.414A2 2 0 0 1 4 12.172V6.415a1.002 1.002 0 0 1 1.707-.707L20 20.009",
    "m12.214 3.381 8.414 14.966a1 1 0 0 1-.167 1.199l-1.168 1.163a1 1 0 0 1-.706.291H6.351a1 1 0 0 1-.625-.219L3.25 18.8a1 1 0 0 1 .631-1.781l4.165.027",
  ],
  globe: [
    "M2 12a10 10 0 1 0 20 0a10 10 0 1 0-20 0",
    "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",
    "M2 12h20",
  ],
  lightbulb: [
    "M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5",
    "M9 18h6",
    "M10 22h4",
  ],
  pen: [
    "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",
    "M13 21h8",
  ],
  telescope: [
    "m10.065 12.493-6.18 1.318a.934.934 0 0 1-1.108-.702l-.537-2.15a1.07 1.07 0 0 1 .691-1.265l13.504-4.44",
    "M16.485 5.94a2 2 0 0 1 1.455-2.425l1.09-.272a1 1 0 0 1 1.212.727l1.515 6.06a1 1 0 0 1-.727 1.213l-1.09.272a2 2 0 0 1-2.425-1.455z",
    "m13.56 11.747 4.332-.924",
    "m6.158 8.633 1.114 4.456",
    "M10 13a2 2 0 1 0 4 0a2 2 0 1 0-4 0",
    "m8 21 3.105-6.21",
    "m16 21-3.105-6.21",
  ],
} satisfies Record<string, string[]>;

export type DrawingIconName = keyof typeof DRAWING_ICONS;

export interface DrawingIconProps {
  /** A preset name, or your own list of 24×24 stroke path `d` strings, drawn in order. */
  icon: DrawingIconName | string[];
  /** Rendered size in px. */
  size?: number;
  /** Length of one draw → hold → erase loop, in seconds. */
  duration?: number;
  /** Delay between consecutive strokes starting, in seconds. */
  stagger?: number;
  /** Keep a faint copy of the full icon underneath so its shape is always readable. */
  ghost?: boolean;
  /** Pause on the fully drawn icon (e.g. when the work it stands for finishes). */
  paused?: boolean;
  /** Accessible label. Omit when a visible text label sits next to the icon. */
  label?: string;
  className?: string;
}

/*
 * Every path gets pathLength="1", so a dasharray of 1 is exactly one stroke
 * long whatever the real geometry: offset 1 hides it, 0 shows it, -1 slides
 * it off the far end. One keyframe set then draws any shape.
 */
const STYLES = `
@keyframes drawing-icon-stroke {
  0% { stroke-dashoffset: 1; }
  45%, 65% { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -1; }
}
.drawing-icon [data-stroke] {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: drawing-icon-stroke var(--draw-duration) ease-in-out infinite;
  animation-delay: calc(var(--i) * var(--draw-stagger));
}
.drawing-icon[data-paused] [data-stroke] {
  animation: none;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 0.3s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .drawing-icon [data-stroke] {
    animation: none;
    stroke-dashoffset: 0;
  }
  .drawing-icon:not([data-paused]) {
    animation: drawing-icon-pulse 1.6s ease-in-out infinite;
  }
}
@keyframes drawing-icon-pulse {
  50% { opacity: 0.45; }
}
`;

export function DrawingIcon({
  icon,
  size = 16,
  duration = 3.6,
  stagger = 0.3,
  ghost = false,
  paused = false,
  label,
  className,
}: DrawingIconProps) {
  const paths = typeof icon === "string" ? DRAWING_ICONS[icon] : icon;

  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      role={label ? "img" : undefined}
      aria-label={label}
      aria-hidden={label ? undefined : true}
      data-paused={paused ? "" : undefined}
      className={cn("drawing-icon shrink-0", className)}
      style={
        {
          "--draw-duration": `${duration}s`,
          "--draw-stagger": `${stagger}s`,
        } as React.CSSProperties
      }
    >
      <style>{STYLES}</style>
      {ghost && (
        <g opacity={0.15}>
          {paths.map((d, i) => (
            <path key={i} d={d} />
          ))}
        </g>
      )}
      {paths.map((d, i) => (
        <path
          key={i}
          d={d}
          pathLength={1}
          data-stroke=""
          style={{ "--i": i } as React.CSSProperties}
        />
      ))}
    </svg>
  );
}
```
