# Wire Postcard Spinner Rack Photo Carousel That Spins with Inertia and Shows Card Backs

> 3D carousel built as a shop postcard rack: portraits in wire pockets round a pole that spins with inertia and shows card backs. Free React and HTML code.

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/wire-postcard-spinner-rack-photo-carousel-that-spins-with-inertia-and-shows-card-backs).

- Page, with a live preview: https://shadcn.exchange/r/weinc/wire-postcard-spinner-rack-photo-carousel-that-spins-with-inertia-and-shows-card-backs
- Screenshot: https://shadcn.exchange/shot/weinc/wire-postcard-spinner-rack-photo-carousel-that-spins-with-inertia-and-shows-card-backs/light.png
- Registry item: https://we.inc/r/wire-postcard-spinner-rack-photo-carousel-that-spins-with-inertia-and-shows-card-backs.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/wire-postcard-spinner-rack-photo-carousel-that-spins-with-inertia-and-shows-card-backs.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `framer-motion`

## Files

### `components/we-inc/wire-postcard-spinner-rack-photo-carousel-that-spins-with-inertia-and-shows-card-backs.tsx`

```tsx
import { useCallback, useEffect, useRef, useState } from "react";
import { useReducedMotion } from "framer-motion";


/** Eight columns of the rack. Each holds two postcards: one in the upper pocket, one in the lower. */
const columns = [
  {
    top: { title: "Tangerine", src: "https://images.pexels.com/photos/39325164/pexels-photo-39325164.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman in an orange top seated against a vivid orange backdrop", tone: "#e8440f", focus: "50% 30%" },
    bottom: { title: "Lime room", src: "https://images.pexels.com/photos/7659286/pexels-photo-7659286.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Figure in a long dark coat turned away against a lime green backdrop", tone: "#4fa312", focus: "50% 40%" },
  },
  {
    top: { title: "Half sun", src: "https://images.pexels.com/photos/7516560/pexels-photo-7516560.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman in a red dress standing in profile against a yellow and red backdrop", tone: "#e9b416", focus: "50% 35%" },
    bottom: { title: "Rose veil", src: "https://images.pexels.com/photos/31301594/pexels-photo-31301594.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman in a black dress parting sheer red curtains", tone: "#b3214a", focus: "50% 35%" },
  },
  {
    top: { title: "Magenta lean", src: "https://images.pexels.com/photos/21352926/pexels-photo-21352926.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman in a green top and black jacket against a magenta backdrop", tone: "#c4104d", focus: "50% 30%" },
    bottom: { title: "Pearls", src: "https://images.pexels.com/photos/35766171/pexels-photo-35766171.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman in sunglasses, pearls and a yellow top against a black backdrop", tone: "#17151a", focus: "50% 35%" },
  },
  {
    top: { title: "Green curtain", src: "https://images.pexels.com/photos/32174358/pexels-photo-32174358.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman in a teal suit standing between deep green curtains", tone: "#0c6b3a", focus: "50% 40%" },
    bottom: { title: "Black lace", src: "https://images.pexels.com/photos/27834206/pexels-photo-27834206.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman in glasses and a black lace jacket against a yellow backdrop", tone: "#e39a10", focus: "50% 35%" },
  },
  {
    top: { title: "Painted denim", src: "https://images.pexels.com/photos/27820190/pexels-photo-27820190.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Man in a paint-splashed denim jacket against a yellow and white backdrop", tone: "#e6b21b", focus: "50% 30%" },
    bottom: { title: "Red hour", src: "https://images.pexels.com/photos/14668786/pexels-photo-14668786.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman in a sheer black top lit red against a crimson backdrop", tone: "#b3122f", focus: "50% 35%" },
  },
  {
    top: { title: "Teal and red", src: "https://images.pexels.com/photos/16677542/pexels-photo-16677542.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Close portrait of a woman with red lips lit in teal and red", tone: "#0d5a4c", focus: "50% 35%" },
    bottom: { title: "Roll neck", src: "https://images.pexels.com/photos/35514244/pexels-photo-35514244.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Man in a dark roll neck resting his hand on his temple, lit in blue and red", tone: "#3a1420", focus: "50% 30%" },
  },
  {
    top: { title: "Blue cheek", src: "https://images.pexels.com/photos/17784344/pexels-photo-17784344.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman lit in blue on one side against an orange backdrop", tone: "#d2611f", focus: "45% 35%" },
    bottom: { title: "Profile, amber", src: "https://images.pexels.com/photos/7658850/pexels-photo-7658850.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman in profile resting her chin on her hand in amber and green light", tone: "#c26a22", focus: "50% 35%" },
  },
  {
    top: { title: "Jump", src: "https://images.pexels.com/photos/794062/pexels-photo-794062.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman mid-stride in a checked coat against an orange backdrop", tone: "#e8930f", focus: "50% 35%" },
    bottom: { title: "Warm wall", src: "https://images.pexels.com/photos/2872643/pexels-photo-2872643.jpeg?auto=compress&cs=tinysrgb&w=420", alt: "Woman in sunglasses and an orange top against a sunlit yellow wall", tone: "#e3a73c", focus: "50% 30%" },
  },
];

const N = columns.length;
const STEP = (Math.PI * 2) / N;
// Design pixels; the whole rack is scaled to fit its box.
const CW = 140; // postcard width
const CH = 198; // postcard height
const R = 214; // distance from the pole to the face of a card
const TIER_Y = [92, 332]; // top edge of the upper and lower pockets
const POLE_TOP = 34;
const BASE_Y = 596;
const SCENE_W = 620;
const SCENE_H = 700;
const HOLD_MS = 3000;

const mod = (a: number, n: number) => ((a % n) + n) % n;
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));

const WIRE = "#17171a";
const wireGrad = "linear-gradient(180deg, #55565c 0%, #17171a 45%, #050506 100%)";

type Card = { title: string; src: string; alt: string; tone: string; focus: string };

function Postcard({ card, plate, live, refs, slot }: { card: Card; plate: number; live: boolean; refs: React.MutableRefObject<Record<string, HTMLDivElement | null>>; slot: string }) {
  return (
    <>
      {/* printed side */}
      <div className="absolute inset-0 overflow-hidden rounded-[3px] bg-[#fbfaf6]" style={{ backfaceVisibility: "hidden", boxShadow: "0 0 0 0.5px rgba(0,0,0,0.25)" }}>
        <div className="absolute inset-[7px] overflow-hidden" style={{ background: card.tone }}>
          <img src={card.src} alt={live ? card.alt : ""} width={CW - 14} height={CH - 14} loading="eager" decoding="async" draggable={false} className="h-full w-full object-cover" style={{ objectPosition: card.focus }} />
        </div>
        <div
          ref={(el) => {
            refs.current[`${slot}g`] = el;
          }}
          className="pointer-events-none absolute inset-0"
          style={{ background: "linear-gradient(104deg, rgba(255,255,255,0) 30%, rgba(255,255,255,0.55) 47%, rgba(255,255,255,0.12) 55%, rgba(255,255,255,0) 70%)", opacity: 0 }}
        />
        <div
          ref={(el) => {
            refs.current[`${slot}f`] = el;
          }}
          className="pointer-events-none absolute inset-0 bg-[#1b1206]"
          style={{ opacity: 0 }}
        />
      </div>
      {/* written side, seen when the card is on the far side of the rack */}
      <div aria-hidden="true" className="absolute inset-0 overflow-hidden rounded-[3px] bg-[#e9dfcb]" style={{ backfaceVisibility: "hidden", transform: "rotateY(180deg)", boxShadow: "0 0 0 0.5px rgba(0,0,0,0.25)" }}>
        <div className="absolute left-1/2 top-[14px] h-[92px] w-px bg-neutral-500/60" />
        <div className="absolute right-[10px] top-[10px] h-[30px] w-[24px] border border-dashed border-neutral-500/80" />
        {[52, 66, 80, 94].map((y) => (
          <div key={y} className="absolute right-[10px] h-px w-[50px] bg-neutral-500/60" style={{ top: y }} />
        ))}
        <div className="absolute left-[10px] top-[12px] font-mono text-[7px] uppercase leading-[1.5] tracking-[0.14em] text-neutral-600">
          Plate {String(plate).padStart(2, "0")}
          <br />
          {card.title}
        </div>
        <div
          ref={(el) => {
            refs.current[`${slot}b`] = el;
          }}
          className="pointer-events-none absolute inset-0 bg-[#1b1206]"
          style={{ opacity: 0.4 }}
        />
      </div>
    </>
  );
}

export default function PostcardSpinnerRack() {
  const reduce = useReducedMotion();
  const wrapRef = useRef<HTMLDivElement>(null);
  const sceneRef = useRef<HTMLDivElement>(null);
  const rotorRef = useRef<HTMLDivElement>(null);
  const rackRef = useRef<HTMLDivElement>(null);
  const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
  const shade = useRef<Record<string, HTMLDivElement | null>>({});
  const [active, setActive] = useState(0);
  const sim = useRef({
    th: 1.6 * STEP,
    vel: 0,
    target: 0,
    free: false,
    dragging: false,
    lastX: 0,
    lastT: 0,
    scale: 1,
    paused: false,
    nextAuto: 0,
    active: 0,
    lean: new Array(N * 2).fill(0) as number[],
    leanV: new Array(N * 2).fill(0) as number[],
    wob: 0,
    wobV: 0,
  });

  const step = useCallback((dir: number) => {
    const s = sim.current;
    s.free = false;
    s.target = Math.round(s.target / STEP) * STEP - dir * STEP;
    s.nextAuto = performance.now() + HOLD_MS * 2;
  }, []);

  useEffect(() => {
    const wrap = wrapRef.current;
    const scene = sceneRef.current;
    const rotor = rotorRef.current;
    const rack = rackRef.current;
    if (!wrap || !scene || !rotor || !rack) return;
    const s = sim.current;
    let raf = 0;
    let visible = true;
    let last = performance.now();
    s.nextAuto = last + 2600;
    if (reduce) s.th = 0;

    const measure = () => {
      const narrow = wrap.clientWidth < 520;
      const sc = clamp(wrap.clientWidth / 610, 0.56, 1.12);
      s.scale = sc;
      wrap.style.height = `${Math.round(SCENE_H * sc) + (narrow ? 64 : 0)}px`;
      scene.style.transform = `translateX(-50%) scale(${sc.toFixed(4)})`;
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(wrap);

    const paint = () => {
      rotor.style.transform = `rotateY(${s.th.toFixed(5)}rad)`;
      rack.style.transform = `rotateX(-7deg) rotateZ(${s.wob.toFixed(3)}deg)`;
      for (let i = 0; i < N; i++) {
        let phi = mod(s.th + i * STEP + Math.PI, Math.PI * 2) - Math.PI;
        const f = Math.cos(phi);
        const front = clamp(0.5 * (1 - f), 0, 0.6);
        const back = clamp(0.1 + 0.34 * (1 + f), 0, 0.5);
        const gloss = 0.5 * Math.exp(-Math.pow((phi - 0.62) / 0.3, 2));
        for (let t = 0; t < 2; t++) {
          const k = i * 2 + t;
          const c = cardRefs.current[k];
          if (c) c.style.transform = `rotateZ(${s.lean[k].toFixed(3)}deg)`;
          const fs = shade.current[`${k}f`];
          const bs = shade.current[`${k}b`];
          const gs = shade.current[`${k}g`];
          if (fs) fs.style.opacity = front.toFixed(3);
          if (bs) bs.style.opacity = back.toFixed(3);
          if (gs) gs.style.opacity = gloss.toFixed(3);
        }
        phi = 0;
      }
      const a = mod(Math.round(-s.th / STEP), N);
      if (a !== s.active) {
        s.active = a;
        setActive(a);
      }
    };

    const tick = (now: number) => {
      const dt = Math.min(0.033, Math.max(0.001, (now - last) / 1000));
      last = now;
      const v0 = s.vel;
      if (reduce) {
        s.th = s.target;
        s.vel = 0;
      } else if (!s.dragging) {
        if (s.free) {
          s.th += s.vel * dt;
          s.vel *= Math.exp(-1.25 * dt);
          if (Math.abs(s.vel) < 0.9) {
            s.free = false;
            s.target = Math.round((s.th + s.vel * 0.18) / STEP) * STEP;
          }
        } else {
          const acc = 34 * (s.target - s.th) - 7.4 * s.vel;
          s.vel += acc * dt;
          s.th += s.vel * dt;
          if (!s.paused && now > s.nextAuto) {
            s.target = Math.round(s.target / STEP) * STEP - STEP;
            s.nextAuto = now + HOLD_MS;
          }
        }
      }
      if (!reduce) {
        // the cards lag behind every change of speed, then swing back into their pockets
        const alpha = clamp((s.vel - v0) / dt, -60, 60);
        for (let k = 0; k < N * 2; k++) {
          const loose = 0.75 + 0.5 * (((k * 37) % 11) / 10);
          const a = -150 * s.lean[k] - (9 + loose * 3) * s.leanV[k] - alpha * 2.6 * loose;
          s.leanV[k] += a * dt;
          s.lean[k] = clamp(s.lean[k] + s.leanV[k] * dt, -9, 9);
        }
        const wa = -120 * s.wob - 9 * s.wobV - alpha * 0.28;
        s.wobV += wa * dt;
        s.wob = clamp(s.wob + s.wobV * dt, -1.6, 1.6);
      }
      paint();
      if (visible) raf = requestAnimationFrame(tick);
    };

    const io = new IntersectionObserver(([e]) => {
      const was = visible;
      visible = e.isIntersecting;
      if (visible && !was) {
        last = performance.now();
        raf = requestAnimationFrame(tick);
      }
    });
    io.observe(wrap);
    raf = requestAnimationFrame(tick);

    const down = (e: PointerEvent) => {
      if (e.button !== 0 || !(e.target as HTMLElement).closest("[data-rack]")) return;
      s.dragging = true;
      s.free = false;
      s.lastX = e.clientX;
      s.lastT = performance.now();
      scene.setPointerCapture(e.pointerId);
    };
    const move = (e: PointerEvent) => {
      if (!s.dragging) return;
      const now = performance.now();
      const d = (e.clientX - s.lastX) / (R * s.scale);
      s.th += d;
      const dt = Math.max(1, now - s.lastT) / 1000;
      s.vel = s.vel * 0.6 + (d / dt) * 0.4;
      s.lastX = e.clientX;
      s.lastT = now;
    };
    const up = () => {
      if (!s.dragging) return;
      s.dragging = false;
      s.vel = clamp(s.vel, -14, 14);
      if (Math.abs(s.vel) > 0.9) s.free = true;
      else s.target = Math.round(s.th / STEP) * STEP;
      s.nextAuto = performance.now() + HOLD_MS * 2.5;
    };
    scene.addEventListener("pointerdown", down);
    scene.addEventListener("pointermove", move);
    scene.addEventListener("pointerup", up);
    scene.addEventListener("pointercancel", up);

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      io.disconnect();
      scene.removeEventListener("pointerdown", down);
      scene.removeEventListener("pointermove", move);
      scene.removeEventListener("pointerup", up);
      scene.removeEventListener("pointercancel", up);
    };
  }, [reduce]);

  const onKey = (e: React.KeyboardEvent) => {
    if (e.key === "ArrowRight") {
      e.preventDefault();
      step(1);
    } else if (e.key === "ArrowLeft") {
      e.preventDefault();
      step(-1);
    }
  };
  const pause = (v: boolean) => () => {
    sim.current.paused = v;
    if (!v) sim.current.nextAuto = performance.now() + HOLD_MS;
  };

  const hoopR = R - 7;
  const hoops = [TIER_Y[0] + CH * 0.5, TIER_Y[0] + CH + 5, TIER_Y[1] + CH * 0.5, TIER_Y[1] + CH + 5];
  const btn = "grid h-10 w-10 place-items-center rounded-full border border-neutral-900/20 bg-white/50 text-neutral-900 transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-900";
  const col = columns[active];

  return (
    <section className="relative overflow-hidden bg-[#e9e3d6]" onMouseEnter={pause(true)} onMouseLeave={pause(false)} onFocus={pause(true)} onBlur={pause(false)}>
      {/* studio sweep: a soft pool of light behind the rack, falling off to the corners */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0" style={{ background: "radial-gradient(58% 60% at 50% 42%, #faf7f0 0%, #efeadf 42%, #ddd5c5 100%)" }} />
      <div
        ref={wrapRef}
        role="group"
        aria-roledescription="carousel"
        aria-label="Spinner rack of postcards. Use the left and right arrow keys to turn the rack."
        tabIndex={0}
        onKeyDown={onKey}
        className="relative mx-auto w-full overflow-hidden outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-neutral-900/70"
        style={{ height: SCENE_H }}
      >
        <div
          ref={sceneRef}
          className="absolute left-1/2 top-0 select-none"
          style={{ width: SCENE_W, height: SCENE_H, transformOrigin: "50% 0", transform: "translateX(-50%)", perspective: "1500px", perspectiveOrigin: "50% 38%" }}
        >
          {/* shadow the rack throws on the floor */}
          <div aria-hidden="true" className="absolute left-1/2 rounded-[50%] bg-[#3b2a12]" style={{ top: BASE_Y + 12, width: 520, height: 78, marginLeft: -260, filter: "blur(22px)", opacity: 0.34 }} />
          <div aria-hidden="true" className="absolute left-1/2 rounded-[50%] bg-[#1d1408]" style={{ top: BASE_Y + 22, width: 250, height: 38, marginLeft: -118, filter: "blur(9px)", opacity: 0.5 }} />

          <div ref={rackRef} data-rack="" className="absolute left-1/2 top-0 touch-pan-y" style={{ transformStyle: "preserve-3d", width: 0, height: 0, transformOrigin: `0 ${BASE_Y}px`, transform: "rotateX(-7deg)", cursor: "grab" }}>
            {/* weighted base */}
            <div aria-hidden="true" className="absolute rounded-full" style={{ left: -122, top: BASE_Y + 8 - 122, width: 244, height: 244, transform: "rotateX(90deg)", background: "#050506" }} />
            <div
              aria-hidden="true"
              className="absolute rounded-full"
              style={{ left: -122, top: BASE_Y - 122, width: 244, height: 244, transform: "rotateX(90deg)", background: "radial-gradient(circle at 42% 30%, #6a6b72 0%, #2a2b2f 34%, #121214 70%, #060607 100%)", boxShadow: "inset 0 0 0 2px rgba(255,255,255,0.14), inset 0 0 26px rgba(0,0,0,0.7)" }}
            />
            {/* pole */}
            <div aria-hidden="true" className="absolute" style={{ left: -5, top: POLE_TOP, width: 10, height: BASE_Y - POLE_TOP, background: "linear-gradient(90deg, #050506 0%, #2f3034 28%, #8d8e94 42%, #26272a 62%, #050506 100%)", borderRadius: 3 }} />
            <div aria-hidden="true" className="absolute rounded-full" style={{ left: -13, top: POLE_TOP - 20, width: 26, height: 26, background: "radial-gradient(circle at 36% 30%, #b9bac0 0%, #4a4b50 28%, #141416 68%, #050506 100%)" }} />
            <div aria-hidden="true" className="absolute" style={{ left: -11, top: BASE_Y - 20, width: 22, height: 22, background: "linear-gradient(90deg, #050506 0%, #3a3b40 30%, #8d8e94 44%, #1c1d1f 66%, #050506 100%)", borderRadius: "3px 3px 0 0" }} />

            <div ref={rotorRef} className="absolute left-0 top-0" style={{ transformStyle: "preserve-3d", width: 0, height: 0 }}>
              {/* wire hoops that carry the pockets, with spokes back to the pole */}
              {hoops.map((y, h) => (
                <div key={y} aria-hidden="true" className="absolute left-0 top-0" style={{ transformStyle: "preserve-3d" }}>
                  <div className="absolute rounded-full" style={{ left: -hoopR, top: y - hoopR, width: hoopR * 2, height: hoopR * 2, transform: "rotateX(90deg)", border: `3px solid ${WIRE}` }} />
                  {h % 2 === 1 &&
                    [0, 1, 2, 3].map((k) => (
                      <div key={k} className="absolute" style={{ left: -hoopR, top: y - 1.25, width: hoopR * 2, height: 2.5, background: wireGrad, transform: `rotateY(${k * 45 + 22.5}deg)` }} />
                    ))}
                </div>
              ))}
              {columns.map((c, i) => (
                <div key={i} className="absolute left-0 top-0" style={{ transformStyle: "preserve-3d", transform: `rotateY(${(i * 360) / N}deg) translateZ(${R}px)` }}>
                  {[c.top, c.bottom].map((card, t) => (
                    <div key={t} className="absolute left-0 top-0" style={{ transformStyle: "preserve-3d" }}>
                      <div
                        ref={(el) => {
                          cardRefs.current[i * 2 + t] = el;
                        }}
                        className="absolute"
                        style={{ left: -CW / 2, top: TIER_Y[t], width: CW, height: CH, transformOrigin: "50% 100%", transformStyle: "preserve-3d" }}
                      >
                        <Postcard card={card} plate={i * 2 + t + 1} live refs={shade} slot={String(i * 2 + t)} />
                      </div>
                      {/* the wire pocket in front of the card */}
                      <div
                        aria-hidden="true"
                        className="absolute"
                        style={{ left: -CW / 2 - 4, top: TIER_Y[t] + CH * 0.7, width: CW + 8, height: CH * 0.3 + 6, transform: "translateZ(5px)", border: `2.5px solid ${WIRE}`, borderRadius: "3px 3px 9px 9px", boxShadow: "inset 0 1px 0 rgba(255,255,255,0.28), 0 3px 4px rgba(0,0,0,0.22)" }}
                      >
                        <div className="absolute inset-x-0 top-[46%] h-[2.5px]" style={{ background: WIRE, boxShadow: "inset 0 1px 0 rgba(255,255,255,0.28)" }} />
                      </div>
                    </div>
                  ))}
                </div>
              ))}
            </div>
          </div>
        </div>

        <p aria-live="polite" className="absolute bottom-5 left-5 font-mono text-[11px] uppercase leading-relaxed tracking-[0.18em] text-neutral-700 sm:bottom-8 sm:left-8">
          <span className="tabular-nums text-neutral-950">{String(active + 1).padStart(2, "0")}</span>
          <span className="mx-2 text-neutral-500">/</span>
          <span className="tabular-nums">{String(N).padStart(2, "0")}</span>
          <span className="block text-neutral-950">{col.top.title}</span>
          <span className="block text-neutral-600">{col.bottom.title}</span>
        </p>
        <div className="absolute bottom-4 right-4 flex gap-2 sm:bottom-7 sm:right-8">
          <button type="button" onClick={() => step(-1)} aria-label="Turn the rack back one column" className={btn}>
            <svg aria-hidden="true" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
              <path d="M12 4l-6 6 6 6" />
            </svg>
          </button>
          <button type="button" onClick={() => step(1)} aria-label="Turn the rack on one column" className={btn}>
            <svg aria-hidden="true" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
              <path d="M8 4l6 6-6 6" />
            </svg>
          </button>
        </div>
      </div>
    </section>
  );
}
```
