{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "infinite-rotating-cards",
  "type": "registry:block",
  "title": "Infinite Rotating Cards",
  "description": "A hero wheel of photo cards set on a large circle so only the top arc shows, spinning slowly so the next idea always comes into view.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/infinite-rotating-cards.tsx",
      "type": "registry:component",
      "target": "components/tweenui/infinite-rotating-cards.tsx",
      "content": "'use client';\n\nimport { useRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nconst DEG2RAD = Math.PI / 180;\n\nfunction CardMark() {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      className=\"size-7 shrink-0 text-[#045f64] dark:text-[#c6f56f]\"\n      aria-hidden=\"true\"\n    >\n      <path\n        d=\"M8 5h2v2H8V5Zm3 3h2v2h-2V8Zm3 3h2v2h-2v-2Zm-3 3h2v2h-2v-2Zm-3 3h2v2H8v-2Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport interface InfiniteRotatingCard {\n  title: string;\n  image: string;\n  imageAlt: string;\n  icon?: ReactNode;\n}\n\nexport interface InfiniteRotatingCardsProps extends Omit<\n  ComponentPropsWithoutRef<'section'>,\n  'title'\n> {\n  /** Section heading. */\n  title?: ReactNode;\n  /** Supporting line under the heading. */\n  description?: string;\n  /** Cards placed around the wheel. */\n  cards?: InfiniteRotatingCard[];\n}\n\nconst DEFAULT_CARDS: InfiniteRotatingCard[] = [\n  {\n    title: 'Brand motion',\n    image: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=480&h=600&fit=crop',\n    imageAlt: 'Abstract 3D brand shapes',\n  },\n  {\n    title: 'Product UI',\n    image: 'https://images.unsplash.com/photo-1551650975-87deedd944c3?w=480&h=600&fit=crop',\n    imageAlt: 'Product interface on a laptop',\n  },\n  {\n    title: 'Launch pages',\n    image: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=480&h=600&fit=crop',\n    imageAlt: 'Team reviewing a launch page',\n  },\n  {\n    title: 'Scroll stories',\n    image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=480&h=600&fit=crop',\n    imageAlt: 'Analytics dashboard',\n  },\n  {\n    title: 'Hover systems',\n    image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=480&h=600&fit=crop',\n    imageAlt: 'Design team at a whiteboard',\n  },\n  {\n    title: 'Type in motion',\n    image: 'https://images.unsplash.com/photo-1617791160505-6f00504e3519?w=480&h=600&fit=crop',\n    imageAlt: 'Abstract motion form',\n  },\n  {\n    title: 'Photo reveals',\n    image: 'https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=480&h=600&fit=crop',\n    imageAlt: 'Code editor on a screen',\n  },\n  {\n    title: 'Hub diagrams',\n    image: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=480&h=600&fit=crop',\n    imageAlt: 'Laptop with code on the screen',\n  },\n];\n\nexport default function InfiniteRotatingCards({\n  title = 'Motion that actually ships',\n  description = 'Cards ride a slow wheel so the next idea is always coming into view.',\n  cards = DEFAULT_CARDS,\n  className,\n  ...props\n}: InfiniteRotatingCardsProps) {\n  const clipRef = useRef<HTMLDivElement>(null);\n  const wheelRef = useRef<HTMLDivElement>(null);\n\n  useGSAP(\n    () => {\n      const clip = clipRef.current;\n      const wheel = wheelRef.current;\n      if (!clip || !wheel) return;\n\n      const nodes = gsap.utils.toArray<HTMLElement>('[data-hero-card]', wheel);\n      if (!nodes.length) return;\n\n      const slice = 360 / nodes.length;\n\n      const layout = () => {\n        const size = clip.offsetWidth;\n        if (!size) return;\n\n        wheel.style.width = `${size}px`;\n        wheel.style.height = `${size}px`;\n\n        const radius = size / 2;\n        const card = nodes[0];\n        const inset = Math.max(card?.offsetWidth ?? 0, card?.offsetHeight ?? 0) / 2;\n        const orbit = Math.max(radius - inset, 0);\n\n        gsap.set(nodes, {\n          x: (i) => radius + orbit * Math.sin(i * slice * DEG2RAD),\n          y: (i) => radius - orbit * Math.cos(i * slice * DEG2RAD),\n          rotation: (i) => i * slice,\n          xPercent: -50,\n          yPercent: -50,\n        });\n      };\n\n      layout();\n      const frame = requestAnimationFrame(layout);\n      const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(layout) : null;\n      observer?.observe(clip);\n\n      if (prefersReducedMotion()) {\n        return () => {\n          cancelAnimationFrame(frame);\n          observer?.disconnect();\n        };\n      }\n\n      const spin = gsap.to(wheel, {\n        rotation: -360,\n        ease: 'none',\n        duration: Math.max(nodes.length * 4, 16),\n        repeat: -1,\n      });\n\n      return () => {\n        cancelAnimationFrame(frame);\n        observer?.disconnect();\n        spin.kill();\n      };\n    },\n    { scope: clipRef, dependencies: [cards.length] }\n  );\n\n  return (\n    <section\n      {...props}\n      data-infinite-rotating-cards\n      className={cn('w-full py-16 motion-reduce:transition-none md:py-20', className)}\n    >\n      <div className=\"mx-auto w-full max-w-[960px] space-y-10 px-4\">\n        <div className=\"space-y-6 text-center\">\n          <h2 className=\"text-2xl leading-[1.15] font-medium tracking-tight text-[#12161F] md:text-3xl lg:text-4xl dark:text-white\">\n            {title}\n          </h2>\n          <p className=\"mx-auto max-w-[400px] text-sm leading-relaxed text-[#045f64]/70 dark:text-[#9fd4d6]/80\">\n            {description}\n          </p>\n        </div>\n\n        <div ref={clipRef} data-hero-clip className=\"relative aspect-square w-full overflow-hidden\">\n          <div ref={wheelRef} data-hero-wheel className=\"absolute top-0 left-0\">\n            {cards.map((card, index) => (\n              <article\n                key={`${card.title}-${index}`}\n                data-hero-card\n                className=\"absolute top-0 left-0 w-32 space-y-2 bg-white p-2 sm:w-36 md:w-40 md:space-y-3 md:p-3 dark:bg-[#12161F]\"\n              >\n                <div className=\"flex items-start justify-between gap-2\">\n                  <h3 className=\"text-sm leading-snug font-medium text-[#12161F] dark:text-white\">\n                    {card.title}\n                  </h3>\n                  {card.icon ?? <CardMark />}\n                </div>\n                <figure className=\"aspect-[4/5] overflow-hidden rounded-md\">\n                  <img src={card.image} alt={card.imageAlt} className=\"size-full object-cover\" />\n                </figure>\n              </article>\n            ))}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/block/infinite-rotating-cards\nThe sample content loads images from images.unsplash.com. Replace them with your own before shipping."
}