-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expand file tree
/
Copy pathMasonry-JS-CSS.json
More file actions
23 lines (23 loc) · 8.06 KB
/
Masonry-JS-CSS.json
File metadata and controls
23 lines (23 loc) · 8.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"$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:component",
"path": "Masonry/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/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 = () => values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue;\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 adjustHeight = 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, containerHeight } = useMemo(() => {\n if (!width) return { grid: [], containerHeight: 0 };\n\n const colHeights = new Array(columns).fill(0);\n const columnWidth = width / columns;\n\n const gridItems = 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\n return {\n grid: gridItems,\n containerHeight: colHeights.length ? Math.max(...colHeights) + 12 : 0\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\" style={adjustHeight ? { height: `${containerHeight}px` } : undefined}>\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"
]
}