{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-arc",
  "title": "Image Arc",
  "description": "Images on a half-circle wheel that turns on its own or with the scroll.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [
    "https://trytweenly.vercel.app/r/use-reduced-motion.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/image-arc/image-arc.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport gsap from \"gsap\"\nimport { ScrollTrigger } from \"gsap/ScrollTrigger\"\nimport { useGSAP } from \"@gsap/react\"\nimport { useReducedMotion } from \"@/registry/new-york/hooks/use-reduced-motion\"\nimport { cn } from \"@/lib/utils\"\n\ngsap.registerPlugin(useGSAP, ScrollTrigger)\n\nexport interface ImageArcItem {\n  src: string\n  alt: string\n}\n\nexport interface ImageArcProps {\n  images: ImageArcItem[]\n  /** \"auto\" spins continuously; \"scroll\" turns the wheel as the page scrolls. Default: \"auto\" */\n  mode?: \"auto\" | \"scroll\"\n  /** Seconds per full revolution in auto mode. Lower = faster. Default: 80 */\n  duration?: number\n  /** Spin direction. Default: \"clockwise\" */\n  direction?: \"clockwise\" | \"counterclockwise\"\n  /** Degrees turned while the section scrolls past, in scroll mode. Default: 120 */\n  scrollRotation?: number\n  /** Wheel radius as a fraction of the container width. Default: 0.42 */\n  radius?: number\n  /** Card width as a fraction of the radius. Default: 0.26 */\n  itemSize?: number\n  /** Card aspect ratio. Default: \"3 / 4\" */\n  aspectRatio?: string\n  /** Space between cards along the circle, in px. Default: 28 */\n  gap?: number\n  /** Corner radius in px. Default: 12 */\n  rounded?: number\n  /** Pause while hovered (auto mode). Default: true */\n  pauseOnHover?: boolean\n  /** Fade the lower edge into the background. Default: true */\n  fade?: boolean\n  /** Scroll container to track in scroll mode (selector or element). */\n  scroller?: string | HTMLElement\n  className?: string\n}\n\nconst tint = (i: number) =>\n  `hsl(${(i * 47) % 360} 42% 52% / 0.4)`\nconst hideBroken = (e: React.SyntheticEvent<HTMLImageElement>) => (e.currentTarget.style.visibility = \"hidden\")\n\nfunction ratio(aspect: string) {\n  const [w, h] = aspect.split(\"/\").map((v) => parseFloat(v))\n  return w && h ? w / h : 0.75\n}\n\nexport function ImageArc({\n  images,\n  mode = \"auto\",\n  duration = 80,\n  direction = \"clockwise\",\n  scrollRotation = 120,\n  radius = 0.42,\n  itemSize = 0.26,\n  aspectRatio = \"3 / 4\",\n  gap = 28,\n  rounded = 12,\n  pauseOnHover = true,\n  fade = true,\n  scroller,\n  className,\n}: ImageArcProps) {\n  const rootRef = useRef<HTMLDivElement>(null)\n  const wheelRef = useRef<HTMLDivElement>(null)\n  const reduced = useReducedMotion()\n  const [width, setWidth] = useState(0)\n\n  useEffect(() => {\n    const root = rootRef.current\n    if (!root) return\n    const ro = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width))\n    ro.observe(root)\n    return () => ro.disconnect()\n  }, [])\n\n  useEffect(() => {\n    rootRef.current?.querySelectorAll(\"img\").forEach((img) => {\n      if (img.complete && img.naturalWidth === 0) img.style.visibility = \"hidden\"\n    })\n  }, [images, width])\n\n  // Geometry: the wheel's center sits below the cards so only the top half shows\n  const R = width * radius\n  const itemW = R * itemSize\n  const itemH = itemW / ratio(aspectRatio)\n  const centerY = R + itemH / 2 + 24\n  const containerH = centerY + R * 0.3\n  // Fill the whole circle so the spin loops seamlessly; repeat images if needed\n  const slots = images.length ? Math.max(images.length, Math.floor((2 * Math.PI * R) / (itemW + gap))) : 0\n  const sign = direction === \"clockwise\" ? 1 : -1\n\n  useGSAP(\n    () => {\n      const wheel = wheelRef.current\n      if (!wheel || reduced || !width) return\n      if (mode === \"auto\") {\n        gsap.to(wheel, { rotation: `+=${360 * sign}`, duration, ease: \"none\", repeat: -1 })\n      } else {\n        gsap.fromTo(\n          wheel,\n          { rotation: 0 },\n          {\n            rotation: scrollRotation * sign,\n            ease: \"none\",\n            scrollTrigger: { trigger: rootRef.current, scroller, start: \"top bottom\", end: \"bottom top\", scrub: 0.6 },\n          }\n        )\n      }\n    },\n    { scope: rootRef, dependencies: [mode, duration, sign, scrollRotation, scroller, reduced, width > 0], revertOnUpdate: true }\n  )\n\n  const setPaused = (paused: boolean) => {\n    if (!pauseOnHover || mode !== \"auto\" || !wheelRef.current) return\n    gsap.getTweensOf(wheelRef.current).forEach((t) => gsap.to(t, { timeScale: paused ? 0 : 1, duration: 0.6 }))\n  }\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\"relative w-full overflow-hidden\", className)}\n      style={{\n        height: width ? containerH : 400,\n        // Alpha mask for edge fading, not a color gradient\n        maskImage: fade ? \"linear-gradient(to bottom, #000 62%, transparent)\" : undefined,\n      }}\n    >\n      {width > 0 && (\n        <div ref={wheelRef} className=\"absolute left-1/2 size-0\" style={{ top: centerY }}>\n          {Array.from({ length: slots }, (_, i) => {\n            const img = images[i % images.length]\n            const angle = (360 / slots) * i\n            return (\n              <div\n                key={i}\n                className=\"absolute left-0 top-0 overflow-hidden shadow-[0_14px_30px_-12px_rgb(0_0_0/0.4)]\"\n                style={{\n                  width: itemW,\n                  height: itemH,\n                  borderRadius: rounded,\n                  background: tint(i % images.length),\n                  // Place on the circle and rotate so each card faces outward\n                  transform: `translate(-50%, -50%) rotate(${angle}deg) translateY(${-R}px)`,\n                }}\n                onPointerEnter={() => setPaused(true)}\n                onPointerLeave={() => setPaused(false)}\n              >\n                <img\n                  src={img.src}\n                  alt={i < images.length ? img.alt : \"\"}\n                  onError={hideBroken}\n                  draggable={false}\n                  className=\"size-full object-cover\"\n                />\n              </div>\n            )\n          })}\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/image-arc",
  "type": "registry:component"
}