{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-trail",
  "type": "registry:component",
  "title": "ImageTrail",
  "description": "Spawns a trail of images along the cursor path on fast movement, each fading + scaling out. Fixed recycled pool, distance-throttled, transform/opacity only.",
  "dependencies": [
    "gsap"
  ],
  "registryDependencies": [],
  "tier": "pro",
  "files": [
    {
      "path": "components/prism/ImageTrail.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport gsap from \"gsap\";\n\n/**\n * ImageTrail — spawns a trail of images along the cursor path on fast movement,\n * each fading + scaling out. Uses a fixed recycled pool (no unbounded nodes),\n * distance throttling, and transform/opacity only. No-ops on coarse pointers and\n * under prefers-reduced-motion.\n */\nexport function ImageTrail({\n  images,\n  className = \"\",\n  threshold = 80,\n  itemSize = 140,\n}: {\n  images: string[];\n  className?: string;\n  /** px the cursor must travel before spawning the next image */\n  threshold?: number;\n  itemSize?: number;\n}) {\n  const ref = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    if (\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches ||\n      !window.matchMedia(\"(pointer: fine)\").matches\n    ) {\n      return;\n    }\n\n    // Build a recycled pool of <img> nodes (2x the images for overlap headroom).\n    const pool: HTMLImageElement[] = [];\n    const poolSize = Math.max(images.length * 2, 8);\n    for (let i = 0; i < poolSize; i++) {\n      const img = document.createElement(\"img\");\n      img.src = images[i % images.length];\n      img.alt = \"\";\n      Object.assign(img.style, {\n        position: \"absolute\",\n        top: \"0\",\n        left: \"0\",\n        width: `${itemSize}px`,\n        height: `${itemSize}px`,\n        objectFit: \"cover\",\n        borderRadius: \"12px\",\n        opacity: \"0\",\n        pointerEvents: \"none\",\n        willChange: \"transform, opacity\",\n      } as Partial<CSSStyleDeclaration>);\n      el.appendChild(img);\n      pool.push(img);\n    }\n\n    let idx = 0;\n    let lastX = 0;\n    let lastY = 0;\n    let primed = false;\n\n    const onMove = (e: PointerEvent) => {\n      const r = el.getBoundingClientRect();\n      const x = e.clientX - r.left;\n      const y = e.clientY - r.top;\n      if (!primed) {\n        lastX = x;\n        lastY = y;\n        primed = true;\n        return;\n      }\n      if (Math.hypot(x - lastX, y - lastY) < threshold) return;\n      lastX = x;\n      lastY = y;\n\n      const node = pool[idx % pool.length];\n      idx++;\n      gsap.killTweensOf(node);\n      gsap.set(node, { x: x - itemSize / 2, y: y - itemSize / 2, opacity: 1, scale: 0.7, rotate: gsap.utils.random(-12, 12) });\n      gsap.to(node, { scale: 1, duration: 0.35, ease: \"power2.out\" });\n      gsap.to(node, { opacity: 0, duration: 0.8, delay: 0.25, ease: \"power2.in\" });\n    };\n\n    el.addEventListener(\"pointermove\", onMove);\n    return () => {\n      el.removeEventListener(\"pointermove\", onMove);\n      pool.forEach((n) => {\n        gsap.killTweensOf(n);\n        n.remove();\n      });\n    };\n  }, [images, threshold, itemSize]);\n\n  return <div ref={ref} className={className} style={{ position: \"relative\", overflow: \"hidden\" }} />;\n}\n",
      "type": "registry:component",
      "target": "components/prism/ImageTrail.tsx"
    }
  ]
}