Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/r/Masonry-JS-CSS.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
{
"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}) => {\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"
"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": [],
Expand Down
Loading