# Isometric Conveyor (`isometric-conveyor`)

> An isometric conveyor — raw blocks ride the belt, pass under the gate, and come out the other side wearing the accent. A pipeline / ETL illustration.

- **Docs:** https://www.mellowui.com/components/isometric-conveyor
- **Markdown:** https://www.mellowui.com/components/isometric-conveyor.md
- **Registry:** https://www.mellowui.com/r/isometric-conveyor.json
- **Tool prompt:** https://www.mellowui.com/api/prompt/isometric-conveyor
- **Categories:** display, 3d, animation
- **Dependencies:** none

## AI prompt

Add an IsometricConveyor component from the mellow library — an isometric conveyor belt. Blocks ride the belt, pass under a gate, and come out the other side recolored to the accent — a pipeline / ETL illustration. Tune `blocks`, `speed` and `accent`.

## Install

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

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `blocks` | `number` | `3` | Number of blocks riding the belt. |
| `speed` | `number` | `46` | Belt speed in world units per second. |
| `accent` | `string` | `"oklch(0.65 0.25 250)"` | Color blocks take on after passing the gate. |
| `size` | `number` | `420` | Rendered width in px. |
| `className` | `string` | — | Additional CSS classes. |

## 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 } from "react";

export interface IsometricConveyorProps {
  /** Number of blocks riding the belt. */
  blocks?: number;
  /** Belt speed in world units per second. */
  speed?: number;
  /** Color blocks take on after passing the gate. */
  accent?: string;
  /** Rendered width in px. */
  size?: number;
  className?: string;
  style?: React.CSSProperties;
}

const iso = (x: number, y: number, z: number): [number, number] => [
  0.866 * (x - y),
  0.5 * (x + y) - z,
];
const poly = (pts: [number, number][]) =>
  pts.map((p) => `${p[0].toFixed(2)},${p[1].toFixed(2)}`).join(" ");

const TRACK_HW = 150;
const TRACK_HH = 26;
const TRACK_D = 10;
const BLOCK_S = 11;
const BLOCK_H = 20;
const GATE_X = 0;

const TOP = "color-mix(in oklab, var(--ink) 6%, var(--background))";
const RIGHT = "color-mix(in oklab, var(--ink) 16%, var(--background))";
const FRONT = "color-mix(in oklab, var(--ink) 26%, var(--background))";
const PLATE = "color-mix(in oklab, var(--ink) 4%, var(--background))";

function boxFaces(cx: number, cy: number, sx: number, sy: number, z0: number, h: number) {
  return {
    top: poly([
      iso(cx - sx, cy - sy, z0 + h),
      iso(cx + sx, cy - sy, z0 + h),
      iso(cx + sx, cy + sy, z0 + h),
      iso(cx - sx, cy + sy, z0 + h),
    ]),
    right: poly([
      iso(cx + sx, cy - sy, z0 + h),
      iso(cx + sx, cy + sy, z0 + h),
      iso(cx + sx, cy + sy, z0),
      iso(cx + sx, cy - sy, z0),
    ]),
    front: poly([
      iso(cx + sx, cy + sy, z0 + h),
      iso(cx - sx, cy + sy, z0 + h),
      iso(cx - sx, cy + sy, z0),
      iso(cx + sx, cy + sy, z0),
    ]),
  };
}

function Box({
  faces,
  topFill = TOP,
  refs,
}: {
  faces: { top: string; right: string; front: string };
  topFill?: string;
  refs?: {
    top: (el: SVGPolygonElement | null) => void;
    right: (el: SVGPolygonElement | null) => void;
    front: (el: SVGPolygonElement | null) => void;
  };
}) {
  const shared = {
    stroke: "var(--ink)",
    strokeWidth: 1,
    vectorEffect: "non-scaling-stroke" as const,
  };
  return (
    <g>
      <polygon ref={refs?.front} points={faces.front} fill={FRONT} strokeOpacity={0.55} {...shared} className="transition-[fill] duration-300" />
      <polygon ref={refs?.right} points={faces.right} fill={RIGHT} strokeOpacity={0.55} {...shared} className="transition-[fill] duration-300" />
      <polygon ref={refs?.top} points={faces.top} fill={topFill} strokeOpacity={0.85} {...shared} strokeWidth={1.3} className="transition-[fill] duration-300" />
    </g>
  );
}

/**
 * An isometric conveyor — raw blocks ride the belt, pass under the gate, and
 * come out the other side wearing the accent. A pipeline / ETL illustration
 * for bento cards.
 */
export function IsometricConveyor({
  blocks = 3,
  speed = 46,
  accent = "oklch(0.65 0.25 250)",
  size = 420,
  className,
  style,
}: IsometricConveyorProps) {
  const blockRefs = useRef<
    {
      top: SVGPolygonElement | null;
      right: SVGPolygonElement | null;
      front: SVGPolygonElement | null;
    }[]
  >([]);

  useEffect(() => {
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const span = 2 * (TRACK_HW - BLOCK_S - 6);
    const start = -(TRACK_HW - BLOCK_S - 6);

    const draw = (t: number) => {
      for (let i = 0; i < blocks; i++) {
        const el = blockRefs.current[i];
        if (!el?.top || !el.right || !el.front) continue;
        const x = start + (((t * 0.001 * speed) + (i * span) / blocks) % span);
        // pop as the block clears the gate
        const pop = Math.exp(-((x - GATE_X - 14) ** 2) / 160) * 6;
        const faces = boxFaces(x, 0, BLOCK_S, BLOCK_S, TRACK_D, BLOCK_H + pop);
        el.top.setAttribute("points", faces.top);
        el.right.setAttribute("points", faces.right);
        el.front.setAttribute("points", faces.front);
        el.top.setAttribute(
          "fill",
          x > GATE_X ? accent : TOP
        );
      }
    };

    if (reduced) {
      draw(8000);
      return;
    }
    let raf = 0;
    const loop = (t: number) => {
      draw(t);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, [blocks, speed, accent]);

  const initialFaces = boxFaces(-1000, 0, BLOCK_S, BLOCK_S, TRACK_D, BLOCK_H);
  const gateFar = boxFaces(GATE_X, -TRACK_HH - 7, 6, 6, 0, 52);
  const gateNear = boxFaces(GATE_X, TRACK_HH + 7, 6, 6, 0, 52);
  const lintel = boxFaces(GATE_X, 0, 7, TRACK_HH + 13, 52, 10);

  // Drawn extents across the whole belt travel: x -152.4..152.4, y -107.5..88.
  // The far block's raised top sets the top edge, the near track corner the bottom;
  // the viewBox below is those bounds plus ~6 units of margin. Keep them in sync.
  return (
    <svg
      role="img"
      aria-label="Conveyor belt: blocks pass through a gate and come out transformed"
      viewBox="-159 -113 318 207"
      preserveAspectRatio="xMidYMid meet"
      className={["block h-auto w-full", className].filter(Boolean).join(" ")}
      style={{ maxWidth: size, aspectRatio: "318 / 207", ...style }}
    >
      {/* extruded track */}
      {[0, 2, 4, 6, 8, TRACK_D].map((z) => (
        <g key={z} transform={`translate(0, ${-z})`}>
          <polygon
            points={poly([
              iso(-TRACK_HW, -TRACK_HH, 0),
              iso(TRACK_HW, -TRACK_HH, 0),
              iso(TRACK_HW, TRACK_HH, 0),
              iso(-TRACK_HW, TRACK_HH, 0),
            ])}
            fill={PLATE}
            stroke="var(--ink)"
            strokeOpacity={z === 0 || z === TRACK_D ? 0.85 : 0.45}
            strokeWidth={z === 0 || z === TRACK_D ? 1.4 : 0.7}
            vectorEffect="non-scaling-stroke"
            className="transition-[fill] duration-300"
          />
        </g>
      ))}

      {/* lane marks */}
      {[-100, -50, 50, 100].map((x) => (
        <line
          key={x}
          x1={iso(x, -TRACK_HH + 6, TRACK_D)[0]}
          y1={iso(x, -TRACK_HH + 6, TRACK_D)[1]}
          x2={iso(x, TRACK_HH - 6, TRACK_D)[0]}
          y2={iso(x, TRACK_HH - 6, TRACK_D)[1]}
          stroke="var(--ink)"
          strokeOpacity={0.18}
          strokeWidth={1}
          vectorEffect="non-scaling-stroke"
        />
      ))}

      {/* far gate pillar behind the blocks */}
      <Box faces={gateFar} />

      {/* blocks */}
      {Array.from({ length: blocks }, (_, i) => (
        <Box
          key={i}
          faces={initialFaces}
          refs={{
            top: (el) => {
              (blockRefs.current[i] ??= { top: null, right: null, front: null }).top = el;
            },
            right: (el) => {
              (blockRefs.current[i] ??= { top: null, right: null, front: null }).right = el;
            },
            front: (el) => {
              (blockRefs.current[i] ??= { top: null, right: null, front: null }).front = el;
            },
          }}
        />
      ))}

      {/* near pillar + lintel in front of the blocks */}
      <Box faces={gateNear} />
      <Box faces={lintel} />
    </svg>
  );
}

export default IsometricConveyor;

```

## Demo

```tsx
"use client";

import React from "react";
import { IsometricConveyor } from "../mellow/isometric-conveyor";

export default function IsometricConveyorDemo() {
  return (
    <div className="flex flex-col items-center gap-4 p-6">
      <div className="w-full max-w-md rounded-2xl border border-[var(--rule)] bg-[rgba(var(--ink-rgb),0.02)] p-6">
        <IsometricConveyor size={400} className="mx-auto w-full" />
        <div className="mt-2">
          <div className="[font-family:var(--font-mono)] text-[0.625rem] font-medium tracking-[0.16em] text-[rgba(var(--ink-rgb),0.45)] uppercase">
            Pipeline
          </div>
          <div className="mt-1 [font-family:var(--font-sans)] text-base font-medium tracking-[-0.01em] text-[var(--ink)]">
            Raw in, refined out
          </div>
        </div>
      </div>
      <p className="text-[0.8125rem] text-[rgba(var(--ink-rgb),0.35)]">
        Blocks pass the gate and come out wearing the accent
      </p>
    </div>
  );
}

```
