{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card-spotlight-grid",
  "type": "registry:block",
  "title": "Card Spotlight Grid",
  "description": "A staggered project grid — tile heights cycle every six cards — where the hovered tile tilts toward the cursor in 3D while its siblings scale back and dim. Pointer-only.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/card-spotlight-grid.tsx",
      "type": "registry:component",
      "target": "components/tweenui/card-spotlight-grid.tsx",
      "content": "'use client';\n\nimport { useRef, type ComponentPropsWithoutRef, type MouseEvent, 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\n/** Tilt and dimming are pointer affordances — skip them on touch. */\nconst canHover = () =>\n  typeof window !== 'undefined' &&\n  window.matchMedia('(hover: hover) and (pointer: fine)').matches &&\n  !prefersReducedMotion();\n\n/** Degrees of tilt at the far edge of a card. */\nconst TILT_MAX = 9;\nconst DIM_SCALE = 0.96;\nconst DIM_OPACITY = 0.5;\n\n/**\n * Card heights cycle every six tiles, so each row of three reads as a staggered\n * skyline instead of a flat band.\n */\nconst HEIGHTS = [\n  'h-[370px] xl:h-[400px]',\n  'h-[450px] xl:h-[550px]',\n  'h-[280px] xl:h-[300px]',\n  'h-[450px] xl:h-[550px]',\n  'h-[280px] xl:h-[300px]',\n  'h-[370px] xl:h-[400px]',\n];\n\nexport interface CardSpotlightItem {\n  /** Project name, shown over the image. */\n  title: string;\n  /** Short line beside the title. */\n  excerpt: string;\n  /** Cover image URL. */\n  image: string;\n  /** Alt text for the cover. */\n  imageAlt: string;\n  /** Link target for the tile. */\n  href?: string;\n}\n\nexport interface CardSpotlightGridProps extends Omit<ComponentPropsWithoutRef<'section'>, 'title'> {\n  /** Section heading. */\n  title?: ReactNode;\n  /** Supporting line under the heading. */\n  description?: string;\n  /** Tiles in the grid. Defaults to a six-project sample. */\n  projects?: CardSpotlightItem[];\n}\n\nconst DEFAULT_TITLE = 'Where ideas become experiences';\n\nconst DEFAULT_DESCRIPTION =\n  'Every project here is built with one goal — performance. From concept to launch, we focus on solutions that drive engagement, conversions, and growth.';\n\nconst DEFAULT_PROJECTS: CardSpotlightItem[] = [\n  {\n    title: 'Northwind',\n    excerpt: 'Brand & site',\n    image: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=800&h=1000&fit=crop',\n    imageAlt: 'Abstract 3D brand shapes',\n  },\n  {\n    title: 'Lumen',\n    excerpt: 'Product UI',\n    image: 'https://images.unsplash.com/photo-1551650975-87deedd944c3?w=800&h=1000&fit=crop',\n    imageAlt: 'Product interface on a laptop',\n  },\n  {\n    title: 'Fieldnote',\n    excerpt: 'Launch page',\n    image: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=1000&fit=crop',\n    imageAlt: 'Team reviewing a launch page',\n  },\n  {\n    title: 'Cadence',\n    excerpt: 'Design system',\n    image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=800&h=1000&fit=crop',\n    imageAlt: 'Design team at a whiteboard',\n  },\n  {\n    title: 'Halo',\n    excerpt: 'Campaign film',\n    image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=1000&fit=crop',\n    imageAlt: 'Analytics dashboard',\n  },\n  {\n    title: 'Atlas',\n    excerpt: 'Data platform',\n    image: 'https://images.unsplash.com/photo-1531403009284-440f080d1e12?w=800&h=1000&fit=crop',\n    imageAlt: 'Workshop session in progress',\n  },\n];\n\nexport default function CardSpotlightGrid({\n  title = DEFAULT_TITLE,\n  description = DEFAULT_DESCRIPTION,\n  projects = DEFAULT_PROJECTS,\n  className,\n  ...props\n}: CardSpotlightGridProps) {\n  const rootRef = useRef<HTMLElement>(null);\n  const cardRefs = useRef<(HTMLDivElement | null)[]>([]);\n\n  useGSAP(\n    () => {\n      const cards = cardRefs.current.filter(Boolean) as HTMLDivElement[];\n      gsap.set(cards, { transformPerspective: 900, transformStyle: 'preserve-3d' });\n\n      if (prefersReducedMotion()) return;\n\n      // `opacity` rather than `autoAlpha`: autoAlpha parks the element at\n      // `visibility: hidden`, which drops it out of the accessibility tree for\n      // as long as the intro is pending.\n      gsap\n        .timeline()\n        .fromTo(\n          '[data-spotlight-intro]',\n          { y: 18, opacity: 0 },\n          { y: 0, opacity: 1, duration: 0.6, ease: 'power3.out', stagger: 0.12 }\n        )\n        .fromTo(\n          cards,\n          { y: 28, opacity: 0 },\n          { y: 0, opacity: 1, duration: 0.7, ease: 'power3.out', stagger: 0.08 },\n          '-=0.3'\n        );\n    },\n    { scope: rootRef, dependencies: [projects] }\n  );\n\n  /** Tilt the hovered card toward the cursor. */\n  const handleMove = (index: number, event: MouseEvent<HTMLDivElement>) => {\n    const card = cardRefs.current[index];\n    if (!card || !canHover()) return;\n\n    const rect = card.getBoundingClientRect();\n    const x = (event.clientX - rect.left) / rect.width;\n    const y = (event.clientY - rect.top) / rect.height;\n\n    gsap.to(card, {\n      rotateY: (x - 0.5) * TILT_MAX * 2,\n      rotateX: (0.5 - y) * TILT_MAX * 2,\n      duration: 0.35,\n      ease: 'power2.out',\n      overwrite: 'auto',\n    });\n  };\n\n  /** Push every other card back so the hovered one reads as lit. */\n  const handleEnter = (index: number) => {\n    if (!canHover()) return;\n\n    cardRefs.current.forEach((card, idx) => {\n      if (!card) return;\n      gsap.to(card, {\n        scale: idx === index ? 1 : DIM_SCALE,\n        opacity: idx === index ? 1 : DIM_OPACITY,\n        duration: 0.7,\n        ease: 'power2.out',\n        overwrite: 'auto',\n      });\n    });\n  };\n\n  const handleLeave = (index: number) => {\n    if (!canHover()) return;\n\n    const card = cardRefs.current[index];\n    if (card) {\n      gsap.to(card, {\n        rotateX: 0,\n        rotateY: 0,\n        duration: 0.45,\n        ease: 'power2.out',\n        overwrite: 'auto',\n      });\n    }\n\n    cardRefs.current.forEach((sibling) => {\n      if (!sibling) return;\n      gsap.to(sibling, {\n        scale: 1,\n        opacity: 1,\n        duration: 0.7,\n        ease: 'power2.out',\n        overwrite: 'auto',\n      });\n    });\n  };\n\n  return (\n    <section\n      ref={rootRef}\n      data-card-spotlight-grid\n      className={cn('w-full px-5 py-16 md:py-24', className)}\n      {...props}\n    >\n      <div className=\"mx-auto max-w-[1290px] space-y-10 md:space-y-16\">\n        <div className=\"space-y-3 text-center\">\n          <h2\n            data-spotlight-intro\n            className=\"text-3xl font-medium tracking-tight text-[#12161f] md:text-5xl dark:text-white\"\n          >\n            {title}\n          </h2>\n          <p\n            data-spotlight-intro\n            className=\"mx-auto max-w-[755px] text-base text-[#18181b]/60 md:text-lg dark:text-white/60\"\n          >\n            {description}\n          </p>\n        </div>\n\n        <div className=\"grid grid-cols-12 items-start gap-y-6 md:gap-6\">\n          {projects.map((project, index) => (\n            <div\n              key={project.title}\n              className=\"col-span-12 md:col-span-6 lg:col-span-4\"\n              onMouseMove={(event) => handleMove(index, event)}\n              onMouseEnter={() => handleEnter(index)}\n              onMouseLeave={() => handleLeave(index)}\n            >\n              <div\n                ref={(node) => {\n                  cardRefs.current[index] = node;\n                }}\n                className={cn(\n                  'relative w-full overflow-hidden rounded-xl will-change-transform',\n                  'motion-reduce:transform-none motion-reduce:opacity-100 motion-reduce:transition-none',\n                  HEIGHTS[index % HEIGHTS.length]\n                )}\n              >\n                <a\n                  href={project.href ?? '#'}\n                  className=\"relative block size-full focus-visible:ring-2 focus-visible:ring-[#045f64] focus-visible:ring-offset-2 focus-visible:outline-none dark:focus-visible:ring-[#c6f56f]\"\n                >\n                  <img\n                    src={project.image}\n                    alt={project.imageAlt}\n                    loading=\"lazy\"\n                    className=\"size-full object-cover\"\n                  />\n                  <span className=\"pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(0,0,0,0)_50%,rgba(0,0,0,0.5)_75%,#000_100%)]\" />\n                  <span className=\"absolute bottom-6 left-6 z-10 flex flex-wrap items-center gap-x-4 gap-y-1 pr-6\">\n                    <span className=\"text-lg font-medium text-white\">{project.title}</span>\n                    <span className=\"text-sm text-white/70\">{project.excerpt}</span>\n                  </span>\n                </a>\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/block/card-spotlight-grid\nThe sample content loads images from images.unsplash.com. Replace them with your own before shipping."
}