{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cube-carousel",
  "description": "Slides live on the faces of a 3D prism that rotates to advance, dipping back in scale mid-turn like it needs room to swing. Drag to spin it freely with snap, or let it auto-rotate; faces are reassigned on the fly so any number of slides fits on four faces.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/new-york/carousel/cube-carousel.tsx",
      "content": "\"use client\";\n\n/**\n * Slides live on the faces of a 3D prism that rotates to advance, dipping back in scale mid-turn like it needs room to swing. Drag to spin it freely with snap, or let it auto-rotate; faces are reassigned on the fly so any number of slides fits on four faces.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\n\nconst SLIDES = [\n  { seed: \"cube-01\", title: \"Terminal City\" },\n  { seed: \"cube-02\", title: \"Salt Flats\" },\n  { seed: \"cube-03\", title: \"Wavelength\" },\n  { seed: \"cube-04\", title: \"Night Market\" },\n  { seed: \"cube-05\", title: \"Overcast\" },\n  { seed: \"cube-06\", title: \"Redshift\" },\n];\n\nconst C = {\n  persp: 1600, // CSS perspective distance (px)\n  w: 440, // face width (px) — also the prism depth\n  h: 300, // face height (px)\n  dip: 0.35, // how hard the prism shrinks mid-rotation\n};\n\nconst imageUrl = (seed: string) => `https://picsum.photos/seed/${seed}/900/620`;\n\nconst CubeCarousel = () => {\n  const scaleRef = useRef<HTMLDivElement>(null);\n  const prismRef = useRef<HTMLDivElement>(null);\n  const rot = useRef(0);\n  const target = useRef(0);\n  const [step, setStep] = useState(0);\n\n  useEffect(() => {\n    let dragging = false;\n    let lastX = 0;\n    let idle = 0;\n\n    let raf = 0;\n    const tick = () => {\n      raf = requestAnimationFrame(tick);\n      if (!dragging && ++idle > 230) {\n        // ~3.8s of stillness → quarter turn to the next face\n        target.current = (Math.round(target.current / 90) + 1) * 90;\n        idle = 0;\n      }\n      rot.current += (target.current - rot.current) * 0.085;\n\n      const prism = prismRef.current;\n      const scaler = scaleRef.current;\n      if (prism && scaler) {\n        prism.style.transform = `translateZ(${-C.w / 2}px) rotateY(${-rot.current}deg)`;\n        // shrink at the halfway point of a turn, full size when settled\n        const frac = Math.abs(rot.current / 90 - Math.round(rot.current / 90));\n        scaler.style.transform = `scale(${1 - Math.min(0.5, frac) * C.dip})`;\n      }\n      setStep((s) => {\n        const sr = Math.round(rot.current / 90);\n        return sr === s ? s : sr;\n      });\n    };\n    tick();\n\n    const onDown = (e: PointerEvent) => {\n      dragging = true;\n      lastX = e.clientX;\n      (e.target as HTMLElement).setPointerCapture?.(e.pointerId);\n    };\n    const onMove = (e: PointerEvent) => {\n      if (!dragging) return;\n      const dx = e.clientX - lastX;\n      lastX = e.clientX;\n      target.current -= dx * 0.28; // px → degrees, unbounded either way\n      idle = 0;\n    };\n    const onUp = () => {\n      if (!dragging) return;\n      dragging = false;\n      target.current = Math.round(target.current / 90) * 90; // settle on a face\n      idle = 0;\n    };\n\n    const scene = prismRef.current?.closest(\"[data-cube]\");\n    scene?.addEventListener(\"pointerdown\", onDown as EventListener);\n    window.addEventListener(\"pointermove\", onMove);\n    window.addEventListener(\"pointerup\", onUp);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      scene?.removeEventListener(\"pointerdown\", onDown as EventListener);\n      window.removeEventListener(\"pointermove\", onMove);\n      window.removeEventListener(\"pointerup\", onUp);\n    };\n  }, []);\n\n  // Four physical faces cycle through N slides: face f shows the slide for the\n  // nearest rotation step k with k ≡ f (mod 4), so neighbours are always ready.\n  const slideForFace = (f: number) => {\n    const k = f + 4 * Math.round((step - f) / 4);\n    return SLIDES[((k % SLIDES.length) + SLIDES.length) % SLIDES.length];\n  };\n\n  return (\n    <div\n      data-cube\n      className=\"relative flex h-[80vh] w-full cursor-grab select-none items-center justify-center overflow-hidden active:cursor-grabbing\"\n      style={{ perspective: `${C.persp}px` }}\n    >\n      <div ref={scaleRef} style={{ willChange: \"transform\" }}>\n        <div\n          ref={prismRef}\n          className=\"relative\"\n          style={{\n            width: C.w,\n            height: C.h,\n            transformStyle: \"preserve-3d\",\n            willChange: \"transform\",\n          }}\n        >\n          {[0, 1, 2, 3].map((f) => {\n            const slide = slideForFace(f);\n            return (\n              <div\n                key={f}\n                className=\"absolute inset-0 overflow-hidden rounded-lg bg-neutral-900 shadow-[0_30px_60px_-15px_rgba(0,0,0,0.4)] ring-1 ring-white/20\"\n                style={{\n                  transform: `rotateY(${f * 90}deg) translateZ(${C.w / 2}px)`,\n                  backfaceVisibility: \"hidden\",\n                }}\n              >\n                {/* eslint-disable-next-line @next/next/no-img-element */}\n                <img\n                  src={imageUrl(slide.seed)}\n                  alt={slide.title}\n                  draggable={false}\n                  className=\"h-full w-full object-cover\"\n                />\n                <div className=\"pointer-events-none absolute inset-0 bg-linear-to-t from-black/55 via-transparent to-transparent\" />\n                <p className=\"pointer-events-none absolute bottom-4 left-5 text-xs font-medium uppercase tracking-[0.25em] text-white/90\">\n                  {slide.title}\n                </p>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n\n      {/* warm the cache so face reassignment never flashes */}\n      <div className=\"hidden\">\n        {SLIDES.map((s) => (\n          // eslint-disable-next-line @next/next/no-img-element\n          <img key={s.seed} src={imageUrl(s.seed)} alt=\"\" aria-hidden />\n        ))}\n      </div>\n    </div>\n  );\n};\n\nexport default CubeCarousel;\n",
      "type": "registry:component",
      "target": "components/pixel-perfect/cube-carousel.tsx"
    }
  ],
  "type": "registry:block"
}