# Isometric Press (`isometric-press`)

> A tiny isometric printing press — blank sheets glide along the bed, the press stamps down, and each page slides out carrying fresh ink to the delivery pile.

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

## AI prompt

Add an IsometricPress component from the mellow library — a tiny isometric printing press that stamps blank sheets and slides them out carrying fresh ink. Tune `size`, `speed` and `accent`. Good as an illustration in a bento card.

## Install

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

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `size` | `number` | `380` | Rendered width in px. |
| `speed` | `number` | `1` | Cycle speed multiplier. |
| `accent` | `string` | `"oklch(0.65 0.25 250)"` | Colour of the freshly printed marks. |
| `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 IsometricPressProps {
  /** Rendered width in px. */
  size?: number;
  /** Cycle speed multiplier. */
  speed?: number;
  /** Colour of the freshly printed marks. */
  accent?: string;
  className?: string;
  style?: React.CSSProperties;
}

// world X -> (0.866, 0.5), world Y -> (-0.866, 0.5), world Z -> screen -Y
const ISO = "matrix(0.866 0.5 -0.866 0.5 0 0)";
const SX = (x: number, y: number) => 0.866 * (x - y);
const SY = (x: number, y: number, z: number) => 0.5 * (x + y) - z;
const P = (x: number, y: number, z: number) =>
  `${SX(x, y).toFixed(1)} ${SY(x, y, z).toFixed(1)}`;

const FACE = "color-mix(in oklab, var(--ink) 5%, var(--background))";
const FACE_TOP = "color-mix(in oklab, var(--ink) 10%, var(--background))";

// bed plate
const BED_HW = 150;
const BED_HH = 44;
const BED_D = 10;
const BED_R = 12;
const BED_RECT = `M ${-BED_HW + BED_R} ${-BED_HH} L ${BED_HW - BED_R} ${-BED_HH} A ${BED_R} ${BED_R} 0 0 1 ${BED_HW} ${-BED_HH + BED_R} L ${BED_HW} ${BED_HH - BED_R} A ${BED_R} ${BED_R} 0 0 1 ${BED_HW - BED_R} ${BED_HH} L ${-BED_HW + BED_R} ${BED_HH} A ${BED_R} ${BED_R} 0 0 1 ${-BED_HW} ${BED_HH - BED_R} L ${-BED_HW} ${-BED_HH + BED_R} A ${BED_R} ${BED_R} 0 0 1 ${-BED_HW + BED_R} ${-BED_HH} Z`;

// press module footprint and height
const PA = 34;
const PB = 52;
const PZ0 = BED_D;
const PH = 54;

// delivery pile — sheets sit one gap apart, the delivered sheet lands in the next slot
const PILE_Z0 = BED_D + 1;
const PILE_GAP = 2.5;
const PILE_TOP = PILE_Z0 + PILE_GAP * 3;

const T_CYCLE = 3.8;
const ease = (u: number) => u * u * (3 - 2 * u);

/** The press module — top face plus the two viewer-facing sides. */
function boxPaths(a: number, b: number, z0: number, h: number) {
  return {
    top: `M ${P(-a, -b, z0 + h)} L ${P(a, -b, z0 + h)} L ${P(a, b, z0 + h)} L ${P(-a, b, z0 + h)} Z`,
    sideX: `M ${P(a, -b, z0)} L ${P(a, b, z0)} L ${P(a, b, z0 + h)} L ${P(a, -b, z0 + h)} Z`,
    sideY: `M ${P(-a, b, z0)} L ${P(a, b, z0)} L ${P(a, b, z0 + h)} L ${P(-a, b, z0 + h)} Z`,
  };
}

/**
 * A tiny isometric printing press — blank sheets glide along the bed, the
 * press stamps down, and each page slides out carrying fresh ink to the
 * delivery pile. For product pages that make things.
 */
export function IsometricPress({
  size = 380,
  speed = 1,
  accent = "oklch(0.65 0.25 250)",
  className,
  style,
}: IsometricPressProps) {
  const backRef = useRef<SVGGElement>(null);
  const frontRef = useRef<SVGGElement>(null);
  const backLinesRef = useRef<SVGGElement>(null);
  const frontLinesRef = useRef<SVGGElement>(null);
  const pressRef = useRef<SVGGElement>(null);

  useEffect(() => {
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const back = backRef.current;
    const front = frontRef.current;
    const backLines = backLinesRef.current;
    const frontLines = frontLinesRef.current;
    const press = pressRef.current;
    if (!back || !front || !backLines || !frontLines || !press) return;

    const place = (p: number) => {
      let x = -140;
      let z = PILE_Z0;
      let dip = 0;
      let lines = 0;

      if (p < 0.32) {
        x = -140 + ease(p / 0.32) * 140;
      } else if (p < 0.5) {
        x = 0;
        dip = Math.sin((Math.PI * (p - 0.32)) / 0.18) * 6;
      } else if (p < 0.84) {
        const u = (p - 0.5) / 0.34;
        x = ease(u) * 104;
        // Lift over the back half of the glide so the sheet arrives already
        // resting in the next pile slot — rising after it lands reads as the
        // page passing up through the stack.
        z = PILE_Z0 + ease(Math.max(0, (u - 0.45) / 0.55)) * (PILE_TOP - PILE_Z0);
        lines = Math.min(1, (p - 0.5) / 0.08);
      } else {
        x = 104;
        lines = 1;
        z = PILE_TOP;
        // rests on the pile for the remainder of the cycle — the top pile sheet
        // sits at PILE_TOP too, so the reset swap is invisible, no fade needed
      }

      const tf = `translate(${SX(x, 0).toFixed(1)} ${SY(x, 0, z).toFixed(1)}) ${ISO}`;
      const showFront = x > 0;
      back.setAttribute("transform", tf);
      front.setAttribute("transform", tf);
      back.setAttribute("opacity", showFront ? "0" : "1");
      front.setAttribute("opacity", showFront ? "1" : "0");
      backLines.setAttribute("opacity", String(lines));
      frontLines.setAttribute("opacity", String(lines));
      press.setAttribute("transform", `translate(0 ${dip.toFixed(1)})`);
    };

    if (reduced) {
      place(0.7); // printed page mid-delivery, press at rest
      return;
    }

    let rafId = 0;
    let last = performance.now();
    let t = 0;
    const loop = (now: number) => {
      t += Math.min((now - last) / 1000, 1 / 20) * speed;
      last = now;
      place((t % T_CYCLE) / T_CYCLE);
      rafId = requestAnimationFrame(loop);
    };
    rafId = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(rafId);
  }, [speed]);

  const box = boxPaths(PA, PB, PZ0, PH);

  const printedMarks = (
    <>
      <rect x={-16} y={-22} width={14} height={14} fill={accent} opacity={0.8} />
      <line x1={4} y1={-18} x2={16} y2={-18} stroke="var(--ink)" strokeWidth={2} />
      <line x1={4} y1={-11} x2={16} y2={-11} stroke="var(--ink)" strokeWidth={2} />
      <line x1={-16} y1={2} x2={16} y2={2} stroke="var(--ink)" strokeWidth={2} />
      <line x1={-16} y1={10} x2={10} y2={10} stroke="var(--ink)" strokeWidth={2} />
      <line x1={-16} y1={18} x2={16} y2={18} stroke="var(--ink)" strokeWidth={2} />
    </>
  );

  const sheet = (
    linesRef: React.Ref<SVGGElement>,
    key: string
  ): React.ReactNode => (
    <React.Fragment key={key}>
      <rect
        x={-26}
        y={-34}
        width={52}
        height={68}
        rx={4}
        fill="var(--background)"
        stroke="var(--ink)"
        strokeWidth={1.2}
        vectorEffect="non-scaling-stroke"
      />
      <g ref={linesRef} opacity={0}>
        {printedMarks}
      </g>
    </React.Fragment>
  );

  // The pile sheets carry the same marks as the delivered page, so the sheet
  // resting at PILE_TOP and the pile's own top sheet are pixel-identical.
  const stackSheet = (z: number, key: number) => (
    <g key={key} transform={`translate(${SX(104, 0)} ${SY(104, 0, z)}) ${ISO}`}>
      {sheet(null, `s${key}`)}
      {printedMarks}
    </g>
  );

  return (
    <div
      role="img"
      aria-label="A miniature printing press stamping pages one by one onto a delivery pile"
      className={["inline-block", className].filter(Boolean).join(" ")}
      style={style}
    >
      <svg
        viewBox="-190 -132 380 250"
        width={size}
        height={(size * 250) / 380}
        aria-hidden="true"
        className="block"
      >
        {/* bed plate, extruded */}
        {[0, 2, 4, 6, 8, 10].map((z) => (
          <g key={z} transform={`translate(0 ${-z}) ${ISO}`}>
            <path
              d={BED_RECT}
              fill={z === BED_D ? FACE : FACE}
              stroke="var(--ink)"
              strokeWidth={z === 0 || z === BED_D ? 1.6 : 0.75}
              strokeOpacity={z === 0 || z === BED_D ? 0.9 : 0.5}
              vectorEffect="non-scaling-stroke"
            />
          </g>
        ))}

        {/* guide rails on the bed */}
        <g transform={`translate(0 ${-BED_D}) ${ISO}`}>
          <line x1={-140} y1={-38} x2={140} y2={-38} stroke="var(--ink)" strokeOpacity={0.3} strokeWidth={1} vectorEffect="non-scaling-stroke" />
          <line x1={-140} y1={38} x2={140} y2={38} stroke="var(--ink)" strokeOpacity={0.3} strokeWidth={1} vectorEffect="non-scaling-stroke" />
        </g>

        {/* incoming sheet (behind the press) */}
        <g ref={backRef} opacity={1} transform={`translate(${SX(-140, 0)} ${SY(-140, 0, BED_D + 1)}) ${ISO}`}>
          {sheet(backLinesRef, "back")}
        </g>

        {/* delivery pile */}
        {stackSheet(PILE_Z0, 0)}
        {stackSheet(PILE_Z0 + PILE_GAP, 1)}
        {stackSheet(PILE_Z0 + PILE_GAP * 2, 2)}
        {stackSheet(PILE_TOP, 3)}

        {/* outgoing sheet (in front of the press) */}
        <g ref={frontRef} opacity={0} transform={`translate(${SX(-140, 0)} ${SY(-140, 0, BED_D + 1)}) ${ISO}`}>
          {sheet(frontLinesRef, "front")}
        </g>

        {/* the press itself */}
        <g ref={pressRef}>
          <path d={box.sideY} fill={FACE} stroke="var(--ink)" strokeWidth={1.4} strokeOpacity={0.9} vectorEffect="non-scaling-stroke" />
          <path d={box.sideX} fill={FACE} stroke="var(--ink)" strokeWidth={1.4} strokeOpacity={0.9} vectorEffect="non-scaling-stroke" />
          <path d={box.top} fill={FACE_TOP} stroke="var(--ink)" strokeWidth={1.6} strokeOpacity={0.9} vectorEffect="non-scaling-stroke" />
          {/* mouth slot on the +x face */}
          <path
            d={`M ${P(PA, -PB + 10, PZ0 + 4)} L ${P(PA, PB - 10, PZ0 + 4)} L ${P(PA, PB - 10, PZ0 + 10)} L ${P(PA, -PB + 10, PZ0 + 10)} Z`}
            fill="rgba(var(--ink-rgb),0.5)"
          />
          {/* gauge on the +y face, drawn in-plane */}
          <g transform={`matrix(0.866 0.5 0 -1 ${SX(-6, PB)} ${SY(-6, PB, PZ0 + 30)})`}>
            <circle cx={0} cy={0} r={9} fill="var(--background)" stroke="var(--ink)" strokeWidth={1.2} vectorEffect="non-scaling-stroke" />
            <line x1={0} y1={0} x2={5} y2={-4} stroke={accent} strokeWidth={1.6} vectorEffect="non-scaling-stroke" />
          </g>
          {/* ink bottle on top */}
          <g transform={`translate(${SX(0, -20)} ${SY(0, -20, PZ0 + PH)}) ${ISO}`}>
            <circle cx={0} cy={0} r={10} fill={accent} opacity={0.85} stroke="var(--ink)" strokeWidth={1.2} vectorEffect="non-scaling-stroke" />
          </g>
        </g>
      </svg>
    </div>
  );
}

export default IsometricPress;

```

## Demo

```tsx
"use client";

import React, { useEffect, useState } from "react";
import { IsometricPress } from "../mellow/isometric-press";


/** The docs preview box is ~340px wide on phones — shrink to fit it. */
function useNarrow() {
  const [narrow, setNarrow] = useState(false);
  useEffect(() => {
    const mq = window.matchMedia("(max-width: 640px)");
    const sync = () => setNarrow(mq.matches);
    sync();
    mq.addEventListener("change", sync);
    return () => mq.removeEventListener("change", sync);
  }, []);
  return narrow;
}

export default function IsometricPressDemo() {
  const narrow = useNarrow();

  return (
    <div className="flex w-full flex-col items-center gap-2 p-3 sm:gap-3 sm:p-6">
      <IsometricPress size={narrow ? 240 : 400} />
      <p className="text-[0.8125rem] text-[rgba(var(--ink-rgb),0.35)]">
        Blank pages in, printed pages out — one stamp at a time
      </p>
    </div>
  );
}

```
