{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "Masonry-JS-CSS",
	"title": "Masonry",
	"description": "Responsive masonry layout with animated reflow + gaps optimization.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "Masonry.css",
			"target": "@components/Masonry.css",
			"content": ".list {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n\n.item-wrapper {\n  position: absolute;\n  will-change: transform, width, height, opacity;\n  padding: 6px;\n  cursor: pointer;\n  top: 0;\n  left: 0;\n}\n\n.item-wrapper > .item-img {\n  position: relative;\n  background-size: cover;\n  background-position: center center;\n  width: 100%;\n  height: 100%;\n  text-transform: uppercase;\n  font-size: 10px;\n  line-height: 10px;\n  border-radius: 10px;\n  box-shadow: 0px 10px 50px -10px rgba(0, 0, 0, 0.2);\n}\n"
		},
		{
			"type": "registry:component",
			"path": "Masonry.jsx",
			"content": "import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\n\nimport './Masonry.css';\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\n    if (animateFrom === 'random') {\n      const directions = ['top', 'bottom', 'left', 'right'];\n      direction = directions[Math.floor(Math.random() * directions.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\n    const colHeights = new Array(columns).fill(0);\n    const columnWidth = width / columns;\n\n    return items.map(child => {\n      const col = colHeights.indexOf(Math.min(...colHeights));\n      const x = columnWidth * col;\n      const height = child.height / 2;\n      const y = colHeights[col];\n\n      colHeights[col] += height;\n\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 animationProps = {\n        x: item.x,\n        y: item.y,\n        width: item.w,\n        height: item.h\n      };\n\n      if (!hasMounted.current) {\n        const initialPos = getInitialPosition(item, index);\n        const initialState = {\n          opacity: 0,\n          x: initialPos.x,\n          y: initialPos.y,\n          width: item.w,\n          height: item.h,\n          ...(blurToFocus && { filter: 'blur(10px)' })\n        };\n\n        gsap.fromTo(selector, initialState, {\n          opacity: 1,\n          ...animationProps,\n          ...(blurToFocus && { filter: 'blur(0px)' }),\n          duration: 0.8,\n          ease: 'power3.out',\n          delay: index * stagger\n        });\n      } else {\n        gsap.to(selector, {\n          ...animationProps,\n          duration: duration,\n          ease: 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 = (e, item) => {\n    const element = e.currentTarget;\n    const selector = `[data-key=\"${item.id}\"]`;\n\n    if (scaleOnHover) {\n      gsap.to(selector, {\n        scale: hoverScale,\n        duration: 0.3,\n        ease: 'power2.out'\n      });\n    }\n\n    if (colorShiftOnHover) {\n      const overlay = element.querySelector('.color-overlay');\n      if (overlay) {\n        gsap.to(overlay, {\n          opacity: 0.3,\n          duration: 0.3\n        });\n      }\n    }\n  };\n\n  const handleMouseLeave = (e, item) => {\n    const element = e.currentTarget;\n    const selector = `[data-key=\"${item.id}\"]`;\n\n    if (scaleOnHover) {\n      gsap.to(selector, {\n        scale: 1,\n        duration: 0.3,\n        ease: 'power2.out'\n      });\n    }\n\n    if (colorShiftOnHover) {\n      const overlay = element.querySelector('.color-overlay');\n      if (overlay) {\n        gsap.to(overlay, {\n          opacity: 0,\n          duration: 0.3\n        });\n      }\n    }\n  };\n\n  return (\n    <div ref={containerRef} className=\"list\">\n      {grid.map(item => {\n        return (\n          <div\n            key={item.id}\n            data-key={item.id}\n            className=\"item-wrapper\"\n            onClick={() => window.open(item.url, '_blank', 'noopener')}\n            onMouseEnter={e => handleMouseEnter(e, item)}\n            onMouseLeave={e => handleMouseLeave(e, item)}\n          >\n            <div className=\"item-img\" style={{ backgroundImage: `url(${item.img})` }}>\n              {colorShiftOnHover && (\n                <div\n                  className=\"color-overlay\"\n                  style={{\n                    position: 'absolute',\n                    top: 0,\n                    left: 0,\n                    width: '100%',\n                    height: '100%',\n                    background: 'linear-gradient(45deg, rgba(255,0,150,0.5), rgba(0,150,255,0.5))',\n                    opacity: 0,\n                    pointerEvents: 'none',\n                    borderRadius: '8px'\n                  }}\n                />\n              )}\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default Masonry;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0"
	]
}