{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "Masonry-JS-TW",
	"title": "Masonry",
	"description": "Responsive masonry layout with animated reflow + gaps optimization.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "Masonry/Masonry.jsx",
			"content": "import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\n\nconst useMedia = (queries, values, defaultValue) => {\n  const get = () => {\n    if (typeof window === 'undefined') return defaultValue;\n    return values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue;\n  };\n\n  const [value, setValue] = useState(get);\n\n  useEffect(() => {\n    const handler = () => setValue(get);\n    queries.forEach(q => matchMedia(q).addEventListener('change', handler));\n    return () => queries.forEach(q => matchMedia(q).removeEventListener('change', handler));\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [queries]);\n\n  return value;\n};\n\nconst useMeasure = () => {\n  const ref = useRef(null);\n  const [size, setSize] = useState({ width: 0, height: 0 });\n\n  useLayoutEffect(() => {\n    if (!ref.current) return;\n    const ro = new ResizeObserver(([entry]) => {\n      const { width, height } = entry.contentRect;\n      setSize({ width, height });\n    });\n    ro.observe(ref.current);\n    return () => ro.disconnect();\n  }, []);\n\n  return [ref, size];\n};\n\nconst preloadImages = async urls => {\n  await Promise.all(\n    urls.map(\n      src =>\n        new Promise(resolve => {\n          const img = new Image();\n          img.src = src;\n          img.onload = img.onerror = () => resolve();\n        })\n    )\n  );\n};\n\nconst Masonry = ({\n  items,\n  ease = 'power3.out',\n  duration = 0.6,\n  stagger = 0.05,\n  animateFrom = 'bottom',\n  scaleOnHover = true,\n  hoverScale = 0.95,\n  blurToFocus = true,\n  colorShiftOnHover = false\n}) => {\n  const columns = useMedia(\n    ['(min-width:1500px)', '(min-width:1000px)', '(min-width:600px)', '(min-width:400px)'],\n    [5, 4, 3, 2],\n    1\n  );\n\n  const [containerRef, { width }] = useMeasure();\n  const [imagesReady, setImagesReady] = useState(false);\n\n  const getInitialPosition = item => {\n    const containerRect = containerRef.current?.getBoundingClientRect();\n    if (!containerRect) return { x: item.x, y: item.y };\n\n    let direction = animateFrom;\n    if (animateFrom === 'random') {\n      const dirs = ['top', 'bottom', 'left', 'right'];\n      direction = dirs[Math.floor(Math.random() * dirs.length)];\n    }\n\n    switch (direction) {\n      case 'top':\n        return { x: item.x, y: -200 };\n      case 'bottom':\n        return { x: item.x, y: window.innerHeight + 200 };\n      case 'left':\n        return { x: -200, y: item.y };\n      case 'right':\n        return { x: window.innerWidth + 200, y: item.y };\n      case 'center':\n        return {\n          x: containerRect.width / 2 - item.w / 2,\n          y: containerRect.height / 2 - item.h / 2\n        };\n      default:\n        return { x: item.x, y: item.y + 100 };\n    }\n  };\n\n  useEffect(() => {\n    preloadImages(items.map(i => i.img)).then(() => setImagesReady(true));\n  }, [items]);\n\n  const grid = useMemo(() => {\n    if (!width) return [];\n    const colHeights = new Array(columns).fill(0);\n    const gap = 16;\n    const totalGaps = (columns - 1) * gap;\n    const columnWidth = (width - totalGaps) / columns;\n\n    return items.map(child => {\n      const col = colHeights.indexOf(Math.min(...colHeights));\n      const x = col * (columnWidth + gap);\n      const height = child.height / 2;\n      const y = colHeights[col];\n\n      colHeights[col] += height + gap;\n      return { ...child, x, y, w: columnWidth, h: height };\n    });\n  }, [columns, items, width]);\n\n  const hasMounted = useRef(false);\n\n  useLayoutEffect(() => {\n    if (!imagesReady) return;\n\n    grid.forEach((item, index) => {\n      const selector = `[data-key=\"${item.id}\"]`;\n      const animProps = { x: item.x, y: item.y, width: item.w, height: item.h };\n\n      if (!hasMounted.current) {\n        const start = getInitialPosition(item);\n        gsap.fromTo(\n          selector,\n          {\n            opacity: 0,\n            x: start.x,\n            y: start.y,\n            width: item.w,\n            height: item.h,\n            ...(blurToFocus && { filter: 'blur(10px)' })\n          },\n          {\n            opacity: 1,\n            ...animProps,\n            ...(blurToFocus && { filter: 'blur(0px)' }),\n            duration: 0.8,\n            ease: 'power3.out',\n            delay: index * stagger\n          }\n        );\n      } else {\n        gsap.to(selector, {\n          ...animProps,\n          duration,\n          ease,\n          overwrite: 'auto'\n        });\n      }\n    });\n\n    hasMounted.current = true;\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [grid, imagesReady, stagger, animateFrom, blurToFocus, duration, ease]);\n\n  const handleMouseEnter = (id, element) => {\n    if (scaleOnHover) {\n      gsap.to(`[data-key=\"${id}\"]`, {\n        scale: hoverScale,\n        duration: 0.3,\n        ease: 'power2.out'\n      });\n    }\n    if (colorShiftOnHover) {\n      const overlay = element.querySelector('.color-overlay');\n      if (overlay) gsap.to(overlay, { opacity: 0.3, duration: 0.3 });\n    }\n  };\n\n  const handleMouseLeave = (id, element) => {\n    if (scaleOnHover) {\n      gsap.to(`[data-key=\"${id}\"]`, {\n        scale: 1,\n        duration: 0.3,\n        ease: 'power2.out'\n      });\n    }\n    if (colorShiftOnHover) {\n      const overlay = element.querySelector('.color-overlay');\n      if (overlay) gsap.to(overlay, { opacity: 0, duration: 0.3 });\n    }\n  };\n\n  return (\n    <div ref={containerRef} className=\"relative w-full h-full\">\n      {grid.map(item => (\n        <div\n          key={item.id}\n          data-key={item.id}\n          className=\"absolute box-content\"\n          style={{ willChange: 'transform, width, height, opacity' }}\n          onClick={() => window.open(item.url, '_blank', 'noopener')}\n          onMouseEnter={e => handleMouseEnter(item.id, e.currentTarget)}\n          onMouseLeave={e => handleMouseLeave(item.id, e.currentTarget)}\n        >\n          <div\n            className=\"relative w-full h-full bg-cover bg-center rounded-[10px] shadow-[0px_10px_50px_-10px_rgba(0,0,0,0.2)] uppercase text-[10px] leading-[10px]\"\n            style={{ backgroundImage: `url(${item.img})` }}\n          >\n            {colorShiftOnHover && (\n              <div className=\"color-overlay absolute inset-0 rounded-[10px] bg-gradient-to-tr from-pink-500/50 to-sky-500/50 opacity-0 pointer-events-none\" />\n            )}\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n};\n\nexport default Masonry;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0"
	]
}