diff --git a/public/r/Masonry-JS-CSS.json b/public/r/Masonry-JS-CSS.json
index 5057d5e2..05c17c98 100644
--- a/public/r/Masonry-JS-CSS.json
+++ b/public/r/Masonry-JS-CSS.json
@@ -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
\n {grid.map(item => {\n return (\n
window.open(item.url, '_blank', 'noopener')}\n onMouseEnter={e => handleMouseEnter(e, item)}\n onMouseLeave={e => handleMouseLeave(e, item)}\n >\n
\n {colorShiftOnHover && (\n
\n )}\n
\n
\n );\n })}\n
\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 \n {grid.map(item => {\n return (\n
window.open(item.url, '_blank', 'noopener')}\n onMouseEnter={e => handleMouseEnter(e, item)}\n onMouseLeave={e => handleMouseLeave(e, item)}\n >\n
\n {colorShiftOnHover && (\n
\n )}\n
\n
\n );\n })}\n
\n );\n};\n\nexport default Masonry;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/Masonry-JS-TW.json b/public/r/Masonry-JS-TW.json
index 42f45b55..0897e053 100644
--- a/public/r/Masonry-JS-TW.json
+++ b/public/r/Masonry-JS-TW.json
@@ -8,7 +8,7 @@
{
"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 = () => 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 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 \n {grid.map(item => (\n
window.open(item.url, '_blank', 'noopener')}\n onMouseEnter={e => handleMouseEnter(item.id, e.currentTarget)}\n onMouseLeave={e => handleMouseLeave(item.id, e.currentTarget)}\n >\n
\n {colorShiftOnHover && (\n
\n )}\n
\n
\n ))}\n
\n );\n};\n\nexport default Masonry;\n"
+ "content": "import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\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 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, containerHeight } = useMemo(() => {\n if (!width) return { grid: [], containerHeight: 0 };\n\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 const gridItems = 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 colHeights[col] += height + gap;\n\n return { ...child, x, y, w: columnWidth, h: height };\n });\n const containerHeight = colHeights.length > 0 ? Math.max(...colHeights) : 0;\n return { grid: gridItems, containerHeight };\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 \n {grid.map(item => (\n
window.open(item.url, '_blank', 'noopener')}\n onMouseEnter={e => handleMouseEnter(item.id, e.currentTarget)}\n onMouseLeave={e => handleMouseLeave(item.id, e.currentTarget)}\n >\n
\n {colorShiftOnHover && (\n
\n )}\n
\n
\n ))}\n
\n );\n};\n\nexport default Masonry;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/Masonry-TS-CSS.json b/public/r/Masonry-TS-CSS.json
index 9804e94c..bf19a3f8 100644
--- a/public/r/Masonry-TS-CSS.json
+++ b/public/r/Masonry-TS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "Masonry/Masonry.tsx",
- "content": "import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\n\nimport './Masonry.css';\n\nconst useMedia = (queries: string[], values: number[], defaultValue: number): number => {\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 }, [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] as const;\n};\n\nconst preloadImages = async (urls: string[]): Promise => {\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\ninterface Item {\n id: string;\n img: string;\n url: string;\n height: number;\n}\n\ninterface GridItem extends Item {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\ninterface MasonryProps {\n items: Item[];\n ease?: string;\n duration?: number;\n stagger?: number;\n animateFrom?: 'bottom' | 'top' | 'left' | 'right' | 'center' | 'random';\n scaleOnHover?: boolean;\n hoverScale?: number;\n blurToFocus?: boolean;\n colorShiftOnHover?: boolean;\n}\n\nconst Masonry: React.FC = ({\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: GridItem) => {\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)] as typeof animateFrom;\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);\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 }, [grid, imagesReady, stagger, animateFrom, blurToFocus, duration, ease]);\n\n const handleMouseEnter = (e: React.MouseEvent, item: GridItem) => {\n const element = e.currentTarget as HTMLElement;\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') as HTMLElement;\n if (overlay) {\n gsap.to(overlay, {\n opacity: 0.3,\n duration: 0.3\n });\n }\n }\n };\n\n const handleMouseLeave = (e: React.MouseEvent, item: GridItem) => {\n const element = e.currentTarget as HTMLElement;\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') as HTMLElement;\n if (overlay) {\n gsap.to(overlay, {\n opacity: 0,\n duration: 0.3\n });\n }\n }\n };\n\n return (\n \n {grid.map(item => {\n return (\n
window.open(item.url, '_blank', 'noopener')}\n onMouseEnter={e => handleMouseEnter(e, item)}\n onMouseLeave={e => handleMouseLeave(e, item)}\n >\n
\n {colorShiftOnHover && (\n
\n )}\n
\n
\n );\n })}\n
\n );\n};\n\nexport default Masonry;\n"
+ "content": "import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\n\nimport './Masonry.css';\n\nconst useMedia = (queries: string[], values: number[], defaultValue: number): number => {\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 }, [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] as const;\n};\n\nconst preloadImages = async (urls: string[]): Promise => {\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\ninterface Item {\n id: string;\n img: string;\n url: string;\n height: number;\n}\n\ninterface GridItem extends Item {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\ninterface MasonryProps {\n items: Item[];\n ease?: string;\n duration?: number;\n stagger?: number;\n animateFrom?: 'bottom' | 'top' | 'left' | 'right' | 'center' | 'random';\n scaleOnHover?: boolean;\n hoverScale?: number;\n blurToFocus?: boolean;\n colorShiftOnHover?: boolean;\n adjustHeight?: boolean;\n}\n\nconst Masonry: React.FC = ({\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: GridItem) => {\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)] as typeof animateFrom;\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 containerHeight = useMemo(() => {\n if (!grid.length) return 0;\n return Math.max(...grid.map(item => item.y + item.h));\n }, [grid]);\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);\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 }, [grid, imagesReady, stagger, animateFrom, blurToFocus, duration, ease]);\n\n const handleMouseEnter = (e: React.MouseEvent, item: GridItem) => {\n const element = e.currentTarget as HTMLElement;\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') as HTMLElement;\n if (overlay) {\n gsap.to(overlay, {\n opacity: 0.3,\n duration: 0.3\n });\n }\n }\n };\n\n const handleMouseLeave = (e: React.MouseEvent, item: GridItem) => {\n const element = e.currentTarget as HTMLElement;\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') as HTMLElement;\n if (overlay) {\n gsap.to(overlay, {\n opacity: 0,\n duration: 0.3\n });\n }\n }\n };\n\n return (\n \n {grid.map(item => {\n return (\n
window.open(item.url, '_blank', 'noopener')}\n onMouseEnter={e => handleMouseEnter(e, item)}\n onMouseLeave={e => handleMouseLeave(e, item)}\n >\n
\n {colorShiftOnHover && (\n
\n )}\n
\n
\n );\n })}\n
\n );\n};\n\nexport default Masonry;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/Masonry-TS-TW.json b/public/r/Masonry-TS-TW.json
index 76fe1837..fed0fc45 100644
--- a/public/r/Masonry-TS-TW.json
+++ b/public/r/Masonry-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "Masonry/Masonry.tsx",
- "content": "import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\n\nconst useMedia = (queries: string[], values: number[], defaultValue: number): number => {\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 }, [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] as const;\n};\n\nconst preloadImages = async (urls: string[]): Promise => {\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\ninterface Item {\n id: string;\n img: string;\n url: string;\n height: number;\n}\n\ninterface GridItem extends Item {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\ninterface MasonryProps {\n items: Item[];\n ease?: string;\n duration?: number;\n stagger?: number;\n animateFrom?: 'bottom' | 'top' | 'left' | 'right' | 'center' | 'random';\n scaleOnHover?: boolean;\n hoverScale?: number;\n blurToFocus?: boolean;\n colorShiftOnHover?: boolean;\n}\n\nconst Masonry: React.FC = ({\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: GridItem) => {\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)] as typeof animateFrom;\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 }, [grid, imagesReady, stagger, animateFrom, blurToFocus, duration, ease]);\n\n const handleMouseEnter = (id: string, element: HTMLElement) => {\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') as HTMLElement;\n if (overlay) gsap.to(overlay, { opacity: 0.3, duration: 0.3 });\n }\n };\n\n const handleMouseLeave = (id: string, element: HTMLElement) => {\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') as HTMLElement;\n if (overlay) gsap.to(overlay, { opacity: 0, duration: 0.3 });\n }\n };\n\n return (\n \n {grid.map(item => (\n
window.open(item.url, '_blank', 'noopener')}\n onMouseEnter={e => handleMouseEnter(item.id, e.currentTarget)}\n onMouseLeave={e => handleMouseLeave(item.id, e.currentTarget)}\n >\n
\n {colorShiftOnHover && (\n
\n )}\n
\n
\n ))}\n
\n );\n};\n\nexport default Masonry;\n"
+ "content": "import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\n\nconst useMedia = (queries: string[], values: number[], defaultValue: number): number => {\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 }, [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] as const;\n};\n\nconst preloadImages = async (urls: string[]): Promise => {\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\ninterface Item {\n id: string;\n img: string;\n url: string;\n height: number;\n}\n\ninterface GridItem extends Item {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\ninterface MasonryProps {\n items: Item[];\n ease?: string;\n duration?: number;\n stagger?: number;\n animateFrom?: 'bottom' | 'top' | 'left' | 'right' | 'center' | 'random';\n scaleOnHover?: boolean;\n hoverScale?: number;\n blurToFocus?: boolean;\n colorShiftOnHover?: boolean;\n adjustHeight?: boolean;\n}\n\nconst Masonry: React.FC = ({\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: GridItem) => {\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)] as typeof animateFrom;\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 containerHeight = useMemo(() => {\n if (!grid.length) return 0;\n return Math.max(...grid.map(item => item.y + item.h));\n }, [grid]);\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 }, [grid, imagesReady, stagger, animateFrom, blurToFocus, duration, ease]);\n\n const handleMouseEnter = (id: string, element: HTMLElement) => {\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') as HTMLElement;\n if (overlay) gsap.to(overlay, { opacity: 0.3, duration: 0.3 });\n }\n };\n\n const handleMouseLeave = (id: string, element: HTMLElement) => {\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') as HTMLElement;\n if (overlay) gsap.to(overlay, { opacity: 0, duration: 0.3 });\n }\n };\n\n return (\n \n {grid.map(item => (\n
window.open(item.url, '_blank', 'noopener')}\n onMouseEnter={e => handleMouseEnter(item.id, e.currentTarget)}\n onMouseLeave={e => handleMouseLeave(item.id, e.currentTarget)}\n >\n
\n {colorShiftOnHover && (\n
\n )}\n
\n
\n ))}\n
\n );\n};\n\nexport default Masonry;\n"
}
],
"registryDependencies": [],
diff --git a/src/constants/code/Components/masonryCode.js b/src/constants/code/Components/masonryCode.js
index e70946eb..9d38c99e 100644
--- a/src/constants/code/Components/masonryCode.js
+++ b/src/constants/code/Components/masonryCode.js
@@ -40,6 +40,7 @@ const items = [
hoverScale={0.95}
blurToFocus={true}
colorShiftOnHover={false}
+ adjustHeight={false}
/>
`,
code,
diff --git a/src/content/Components/Masonry/Masonry.jsx b/src/content/Components/Masonry/Masonry.jsx
index 3340a13b..4f67569e 100644
--- a/src/content/Components/Masonry/Masonry.jsx
+++ b/src/content/Components/Masonry/Masonry.jsx
@@ -57,7 +57,8 @@ const Masonry = ({
scaleOnHover = true,
hoverScale = 0.95,
blurToFocus = true,
- colorShiftOnHover = false
+ colorShiftOnHover = false,
+ adjustHeight = false
}) => {
const columns = useMedia(
['(min-width:1500px)', '(min-width:1000px)', '(min-width:600px)', '(min-width:400px)'],
@@ -102,13 +103,13 @@ const Masonry = ({
preloadImages(items.map(i => i.img)).then(() => setImagesReady(true));
}, [items]);
- const grid = useMemo(() => {
- if (!width) return [];
+ const { grid, containerHeight } = useMemo(() => {
+ if (!width) return { grid: [], containerHeight: 0 };
const colHeights = new Array(columns).fill(0);
const columnWidth = width / columns;
- return items.map(child => {
+ const gridItems = items.map(child => {
const col = colHeights.indexOf(Math.min(...colHeights));
const x = columnWidth * col;
const height = child.height / 2;
@@ -118,6 +119,11 @@ const Masonry = ({
return { ...child, x, y, w: columnWidth, h: height };
});
+
+ return {
+ grid: gridItems,
+ containerHeight: colHeights.length ? Math.max(...colHeights) + 12 : 0
+ };
}, [columns, items, width]);
const hasMounted = useRef(false);
@@ -214,7 +220,7 @@ const Masonry = ({
};
return (
-
+
{grid.map(item => {
return (
{
type: 'boolean',
default: 'false',
description: 'Whether to show a color overlay effect on hover.'
+ },
+ {
+ name: 'adjustHeight',
+ type: 'boolean',
+ default: 'false',
+ description: 'Automatically adjusts container height to fit all items and prevent overlap.'
}
],
[]
@@ -221,6 +227,7 @@ const MasonryDemo = () => {
scaleOnHover={scaleOnHover}
blurToFocus={blurToFocus}
colorShiftOnHover={colorShiftOnHover}
+ autoAdjust={false}
/>
diff --git a/src/tailwind/Components/Masonry/Masonry.jsx b/src/tailwind/Components/Masonry/Masonry.jsx
index 427248f8..58c0bf56 100644
--- a/src/tailwind/Components/Masonry/Masonry.jsx
+++ b/src/tailwind/Components/Masonry/Masonry.jsx
@@ -55,7 +55,8 @@ const Masonry = ({
scaleOnHover = true,
hoverScale = 0.95,
blurToFocus = true,
- colorShiftOnHover = false
+ colorShiftOnHover = false,
+ adjustHeight = false
}) => {
const columns = useMedia(
['(min-width:1500px)', '(min-width:1000px)', '(min-width:600px)', '(min-width:400px)'],
@@ -99,22 +100,25 @@ const Masonry = ({
preloadImages(items.map(i => i.img)).then(() => setImagesReady(true));
}, [items]);
- const grid = useMemo(() => {
- if (!width) return [];
+ const { grid, containerHeight } = useMemo(() => {
+ if (!width) return { grid: [], containerHeight: 0 };
+
const colHeights = new Array(columns).fill(0);
const gap = 16;
const totalGaps = (columns - 1) * gap;
const columnWidth = (width - totalGaps) / columns;
- return items.map(child => {
+ const gridItems = items.map(child => {
const col = colHeights.indexOf(Math.min(...colHeights));
const x = col * (columnWidth + gap);
const height = child.height / 2;
const y = colHeights[col];
-
colHeights[col] += height + gap;
+
return { ...child, x, y, w: columnWidth, h: height };
});
+ const containerHeight = colHeights.length > 0 ? Math.max(...colHeights) : 0;
+ return { grid: gridItems, containerHeight };
}, [columns, items, width]);
const hasMounted = useRef(false);
@@ -190,7 +194,11 @@ const Masonry = ({
};
return (
-
+
{grid.map(item => (
= ({
@@ -82,7 +83,8 @@ const Masonry: React.FC
= ({
scaleOnHover = true,
hoverScale = 0.95,
blurToFocus = true,
- colorShiftOnHover = false
+ colorShiftOnHover = false,
+ adjustHeight = false
}) => {
const columns = useMedia(
['(min-width:1500px)', '(min-width:1000px)', '(min-width:600px)', '(min-width:400px)'],
@@ -145,6 +147,11 @@ const Masonry: React.FC = ({
});
}, [columns, items, width]);
+ const containerHeight = useMemo(() => {
+ if (!grid.length) return 0;
+ return Math.max(...grid.map(item => item.y + item.h));
+ }, [grid]);
+
const hasMounted = useRef(false);
useLayoutEffect(() => {
@@ -238,7 +245,7 @@ const Masonry: React.FC = ({
};
return (
-
+
{grid.map(item => {
return (
= ({
@@ -80,7 +81,8 @@ const Masonry: React.FC
= ({
scaleOnHover = true,
hoverScale = 0.95,
blurToFocus = true,
- colorShiftOnHover = false
+ colorShiftOnHover = false,
+ adjustHeight = false
}) => {
const columns = useMedia(
['(min-width:1500px)', '(min-width:1000px)', '(min-width:600px)', '(min-width:400px)'],
@@ -142,6 +144,11 @@ const Masonry: React.FC = ({
});
}, [columns, items, width]);
+ const containerHeight = useMemo(() => {
+ if (!grid.length) return 0;
+ return Math.max(...grid.map(item => item.y + item.h));
+ }, [grid]);
+
const hasMounted = useRef(false);
useLayoutEffect(() => {
@@ -214,7 +221,11 @@ const Masonry: React.FC = ({
};
return (
-