{ "name": "masonry", "type": "registry:ui", "files": [ { "path": "ui/masonry.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport * as React from \"react\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/base/hooks/use-isomorphic-layout-effect\";\nimport { useComposedRefs } from \"@/registry/bases/base/lib/compose-refs\";\n\nconst NODE_COLOR = {\n RED: 0,\n BLACK: 1,\n SENTINEL: 2,\n} as const;\n\nconst NODE_OPERATION = {\n REMOVE: 0,\n PRESERVE: 1,\n} as const;\n\ntype NodeColor = (typeof NODE_COLOR)[keyof typeof NODE_COLOR];\ntype NodeOperation = (typeof NODE_OPERATION)[keyof typeof NODE_OPERATION];\n\ninterface ListNode {\n index: number;\n high: number;\n next: ListNode | null;\n}\n\ninterface TreeNode {\n max: number;\n low: number;\n high: number;\n color: NodeColor;\n parent: TreeNode;\n right: TreeNode;\n left: TreeNode;\n list: ListNode;\n}\n\ninterface Tree {\n root: TreeNode;\n size: number;\n}\n\nfunction addInterval(treeNode: TreeNode, high: number, index: number): boolean {\n let node: ListNode | null = treeNode.list;\n let prevNode: ListNode | undefined;\n\n while (node) {\n if (node.index === index) return false;\n if (high > node.high) break;\n prevNode = node;\n node = node.next;\n }\n\n if (!prevNode) treeNode.list = { index, high, next: node };\n if (prevNode) prevNode.next = { index, high, next: prevNode.next };\n\n return true;\n}\n\nfunction removeInterval(\n treeNode: TreeNode,\n index: number,\n): NodeOperation | undefined {\n let node: ListNode | null = treeNode.list;\n if (node.index === index) {\n if (node.next === null) return NODE_OPERATION.REMOVE;\n treeNode.list = node.next;\n return NODE_OPERATION.PRESERVE;\n }\n\n let prevNode: ListNode | undefined = node;\n node = node.next;\n\n while (node !== null) {\n if (node.index === index) {\n prevNode.next = node.next;\n return NODE_OPERATION.PRESERVE;\n }\n prevNode = node;\n node = node.next;\n }\n}\n\nconst SENTINEL_NODE: TreeNode = {\n low: 0,\n max: 0,\n high: 0,\n color: NODE_COLOR.SENTINEL,\n parent: undefined as unknown as TreeNode,\n right: undefined as unknown as TreeNode,\n left: undefined as unknown as TreeNode,\n list: undefined as unknown as ListNode,\n};\n\nSENTINEL_NODE.parent = SENTINEL_NODE;\nSENTINEL_NODE.left = SENTINEL_NODE;\nSENTINEL_NODE.right = SENTINEL_NODE;\n\nfunction updateMax(node: TreeNode) {\n const max = node.high;\n if (node.left === SENTINEL_NODE && node.right === SENTINEL_NODE)\n node.max = max;\n else if (node.left === SENTINEL_NODE)\n node.max = Math.max(node.right.max, max);\n else if (node.right === SENTINEL_NODE)\n node.max = Math.max(node.left.max, max);\n else node.max = Math.max(Math.max(node.left.max, node.right.max), max);\n}\n\nfunction updateMaxUp(node: TreeNode) {\n let x = node;\n\n while (x.parent !== SENTINEL_NODE) {\n updateMax(x.parent);\n x = x.parent;\n }\n}\n\nfunction rotateLeft(tree: Tree, x: TreeNode) {\n if (x.right === SENTINEL_NODE) return;\n const y = x.right;\n x.right = y.left;\n if (y.left !== SENTINEL_NODE) y.left.parent = x;\n y.parent = x.parent;\n\n if (x.parent === SENTINEL_NODE) tree.root = y;\n else if (x === x.parent.left) x.parent.left = y;\n else x.parent.right = y;\n\n y.left = x;\n x.parent = y;\n\n updateMax(x);\n updateMax(y);\n}\n\nfunction rotateRight(tree: Tree, x: TreeNode) {\n if (x.left === SENTINEL_NODE) return;\n const y = x.left;\n x.left = y.right;\n if (y.right !== SENTINEL_NODE) y.right.parent = x;\n y.parent = x.parent;\n\n if (x.parent === SENTINEL_NODE) tree.root = y;\n else if (x === x.parent.right) x.parent.right = y;\n else x.parent.left = y;\n\n y.right = x;\n x.parent = y;\n\n updateMax(x);\n updateMax(y);\n}\n\nfunction replaceNode(tree: Tree, x: TreeNode, y: TreeNode) {\n if (x.parent === SENTINEL_NODE) tree.root = y;\n else if (x === x.parent.left) x.parent.left = y;\n else x.parent.right = y;\n y.parent = x.parent;\n}\n\nfunction fixRemove(tree: Tree, node: TreeNode) {\n let x = node;\n let w: TreeNode;\n\n while (x !== SENTINEL_NODE && x.color === NODE_COLOR.BLACK) {\n if (x === x.parent.left) {\n w = x.parent.right;\n\n if (w.color === NODE_COLOR.RED) {\n w.color = NODE_COLOR.BLACK;\n x.parent.color = NODE_COLOR.RED;\n rotateLeft(tree, x.parent);\n w = x.parent.right;\n }\n\n if (\n w.left.color === NODE_COLOR.BLACK &&\n w.right.color === NODE_COLOR.BLACK\n ) {\n w.color = NODE_COLOR.RED;\n x = x.parent;\n } else {\n if (w.right.color === NODE_COLOR.BLACK) {\n w.left.color = NODE_COLOR.BLACK;\n w.color = NODE_COLOR.RED;\n rotateRight(tree, w);\n w = x.parent.right;\n }\n\n w.color = x.parent.color;\n x.parent.color = NODE_COLOR.BLACK;\n w.right.color = NODE_COLOR.BLACK;\n rotateLeft(tree, x.parent);\n x = tree.root;\n }\n } else {\n w = x.parent.left;\n\n if (w.color === NODE_COLOR.RED) {\n w.color = NODE_COLOR.BLACK;\n x.parent.color = NODE_COLOR.RED;\n rotateRight(tree, x.parent);\n w = x.parent.left;\n }\n\n if (\n w.right.color === NODE_COLOR.BLACK &&\n w.left.color === NODE_COLOR.BLACK\n ) {\n w.color = NODE_COLOR.RED;\n x = x.parent;\n } else {\n if (w.left.color === NODE_COLOR.BLACK) {\n w.right.color = NODE_COLOR.BLACK;\n w.color = NODE_COLOR.RED;\n rotateLeft(tree, w);\n w = x.parent.left;\n }\n\n w.color = x.parent.color;\n x.parent.color = NODE_COLOR.BLACK;\n w.left.color = NODE_COLOR.BLACK;\n rotateRight(tree, x.parent);\n x = tree.root;\n }\n }\n }\n\n x.color = NODE_COLOR.BLACK;\n}\n\nfunction minimumTree(node: TreeNode) {\n let current = node;\n while (current.left !== SENTINEL_NODE) {\n current = current.left;\n }\n return current;\n}\n\nfunction fixInsert(tree: Tree, node: TreeNode) {\n let current = node;\n let y: TreeNode;\n\n while (current.parent.color === NODE_COLOR.RED) {\n if (current.parent === current.parent.parent.left) {\n y = current.parent.parent.right;\n\n if (y.color === NODE_COLOR.RED) {\n current.parent.color = NODE_COLOR.BLACK;\n y.color = NODE_COLOR.BLACK;\n current.parent.parent.color = NODE_COLOR.RED;\n current = current.parent.parent;\n } else {\n if (current === current.parent.right) {\n current = current.parent;\n rotateLeft(tree, current);\n }\n\n current.parent.color = NODE_COLOR.BLACK;\n current.parent.parent.color = NODE_COLOR.RED;\n rotateRight(tree, current.parent.parent);\n }\n } else {\n y = current.parent.parent.left;\n\n if (y.color === NODE_COLOR.RED) {\n current.parent.color = NODE_COLOR.BLACK;\n y.color = NODE_COLOR.BLACK;\n current.parent.parent.color = NODE_COLOR.RED;\n current = current.parent.parent;\n } else {\n if (current === current.parent.left) {\n current = current.parent;\n rotateRight(tree, current);\n }\n\n current.parent.color = NODE_COLOR.BLACK;\n current.parent.parent.color = NODE_COLOR.RED;\n rotateLeft(tree, current.parent.parent);\n }\n }\n }\n tree.root.color = NODE_COLOR.BLACK;\n}\n\ninterface IntervalTree {\n insert(low: number, high: number, index: number): void;\n remove(index: number): void;\n search(\n low: number,\n high: number,\n onCallback: (index: number, low: number) => void,\n ): void;\n size: number;\n}\n\nfunction createIntervalTree(): IntervalTree {\n const tree: Tree = {\n root: SENTINEL_NODE,\n size: 0,\n };\n\n const indexMap: Record = {};\n\n return {\n insert(low, high, index) {\n let x: TreeNode = tree.root;\n let y: TreeNode = SENTINEL_NODE;\n\n while (x !== SENTINEL_NODE) {\n y = x;\n if (low === y.low) break;\n if (low < x.low) x = x.left;\n else x = x.right;\n }\n\n if (low === y.low && y !== SENTINEL_NODE) {\n if (!addInterval(y, high, index)) return;\n y.high = Math.max(y.high, high);\n updateMax(y);\n updateMaxUp(y);\n indexMap[index] = y;\n tree.size++;\n return;\n }\n\n const z: TreeNode = {\n low,\n high,\n max: high,\n color: NODE_COLOR.RED,\n parent: y,\n left: SENTINEL_NODE,\n right: SENTINEL_NODE,\n list: { index, high, next: null },\n };\n\n if (y === SENTINEL_NODE) {\n tree.root = z;\n } else {\n if (z.low < y.low) y.left = z;\n else y.right = z;\n updateMaxUp(z);\n }\n\n fixInsert(tree, z);\n indexMap[index] = z;\n tree.size++;\n },\n\n remove(index) {\n const z = indexMap[index];\n if (z === void 0) return;\n delete indexMap[index];\n\n const intervalResult = removeInterval(z, index);\n if (intervalResult === void 0) return;\n if (intervalResult === NODE_OPERATION.PRESERVE) {\n z.high = z.list.high;\n updateMax(z);\n updateMaxUp(z);\n tree.size--;\n return;\n }\n\n let y = z;\n let originalYColor = y.color;\n let x: TreeNode;\n\n if (z.left === SENTINEL_NODE) {\n x = z.right;\n replaceNode(tree, z, z.right);\n } else if (z.right === SENTINEL_NODE) {\n x = z.left;\n replaceNode(tree, z, z.left);\n } else {\n y = minimumTree(z.right);\n originalYColor = y.color;\n x = y.right;\n\n if (y.parent === z) {\n x.parent = y;\n } else {\n replaceNode(tree, y, y.right);\n y.right = z.right;\n y.right.parent = y;\n }\n\n replaceNode(tree, z, y);\n y.left = z.left;\n y.left.parent = y;\n y.color = z.color;\n }\n\n updateMax(x);\n updateMaxUp(x);\n\n if (originalYColor === NODE_COLOR.BLACK) fixRemove(tree, x);\n tree.size--;\n },\n\n search(low, high, onCallback) {\n const stack = [tree.root];\n while (stack.length !== 0) {\n const node = stack.pop();\n if (!node) continue;\n if (node === SENTINEL_NODE || low > node.max) continue;\n if (node.left !== SENTINEL_NODE) stack.push(node.left);\n if (node.right !== SENTINEL_NODE) stack.push(node.right);\n if (node.low <= high && node.high >= low) {\n let curr: ListNode | null = node.list;\n while (curr !== null) {\n if (curr.high >= low) onCallback(curr.index, node.low);\n curr = curr.next;\n }\n }\n }\n },\n\n get size() {\n return tree.size;\n },\n };\n}\n\ntype CacheKey = string | number | symbol;\ntype CacheConstructor = (new () => Cache) | Record;\n\ninterface Cache {\n set: (k: K, v: V) => V;\n get: (k: K) => V | undefined;\n}\n\nfunction onDeepMemo(\n constructors: CacheConstructor[],\n fn: (...args: T) => U,\n): (...args: T) => U {\n if (!constructors.length || !constructors[0]) {\n throw new Error(\"At least one constructor is required\");\n }\n\n function createCache(obj: CacheConstructor): Cache {\n let cache: Cache;\n if (typeof obj === \"function\") {\n try {\n cache = new (obj as new () => Cache)();\n } catch (_err) {\n cache = new Map();\n }\n } else {\n cache = obj as unknown as Cache;\n }\n return {\n set(k: CacheKey, v: unknown): unknown {\n cache.set(k, v);\n return v;\n },\n get(k: CacheKey): unknown | undefined {\n return cache.get(k);\n },\n };\n }\n\n const depth = constructors.length;\n const baseCache = createCache(constructors[0]);\n\n let base: Cache | undefined;\n let map: Cache | undefined;\n let node: Cache;\n let i: number;\n const one = depth === 1;\n\n function get(args: unknown[]): unknown {\n if (depth < 3) {\n const key = args[0] as CacheKey;\n base = baseCache.get(key) as Cache | undefined;\n return one ? base : base?.get(args[1] as CacheKey);\n }\n\n node = baseCache;\n for (i = 0; i < depth; i++) {\n const next = node.get(args[i] as CacheKey);\n if (!next) return undefined;\n node = next as Cache;\n }\n return node;\n }\n\n function set(args: unknown[], value: unknown): unknown {\n if (depth < 3) {\n if (one) {\n baseCache.set(args[0] as CacheKey, value);\n } else {\n base = baseCache.get(args[0] as CacheKey) as Cache | undefined;\n if (!base) {\n if (!constructors[1]) {\n throw new Error(\n \"Second constructor is required for non-single depth cache\",\n );\n }\n map = createCache(constructors[1]);\n map.set(args[1] as CacheKey, value);\n baseCache.set(args[0] as CacheKey, map);\n } else {\n base.set(args[1] as CacheKey, value);\n }\n }\n return value;\n }\n\n node = baseCache;\n for (i = 0; i < depth - 1; i++) {\n map = node.get(args[i] as CacheKey) as Cache | undefined;\n if (!map) {\n const nextConstructor = constructors[i + 1];\n if (!nextConstructor) {\n throw new Error(`Constructor at index ${i + 1} is required`);\n }\n map = createCache(nextConstructor);\n node.set(args[i] as CacheKey, map);\n node = map;\n } else {\n node = map;\n }\n }\n node.set(args[depth - 1] as CacheKey, value);\n return value;\n }\n\n return (...args: T): U => {\n const cached = get(args);\n if (cached === undefined) {\n return set(args, fn(...args)) as U;\n }\n return cached as U;\n };\n}\n\nconst COLUMN_WIDTH = 200;\nconst GAP = 0;\nconst ITEM_HEIGHT = 300;\nconst OVERSCAN = 2;\nconst SCROLL_FPS = 12;\nconst DEBOUNCE_DELAY = 300;\n\ninterface Positioner {\n columnCount: number;\n columnWidth: number;\n set: (index: number, height: number) => void;\n get: (index: number) => PositionerItem | undefined;\n update: (updates: number[]) => void;\n range: (\n low: number,\n high: number,\n onItemRender: (index: number, left: number, top: number) => void,\n ) => void;\n size: () => number;\n estimateHeight: (itemCount: number, defaultItemHeight: number) => number;\n shortestColumn: () => number;\n all: () => PositionerItem[];\n}\n\ninterface PositionerItem {\n top: number;\n left: number;\n height: number;\n columnIndex: number;\n}\n\ninterface UsePositionerOptions {\n width: number;\n columnWidth?: number;\n columnGap?: number;\n rowGap?: number;\n columnCount?: number;\n maxColumnCount?: number;\n linear?: boolean;\n}\n\nfunction usePositioner(\n {\n width,\n columnWidth = COLUMN_WIDTH,\n columnGap = GAP,\n rowGap,\n columnCount,\n maxColumnCount,\n linear = false,\n }: UsePositionerOptions,\n deps: React.DependencyList = [],\n): Positioner {\n const initPositioner = React.useCallback((): Positioner => {\n function binarySearch(a: number[], y: number): number {\n let l = 0;\n let h = a.length - 1;\n\n while (l <= h) {\n const m = (l + h) >>> 1;\n const x = a[m];\n if (x === y) return m;\n if (x === undefined || x <= y) l = m + 1;\n else h = m - 1;\n }\n\n return -1;\n }\n\n const computedColumnCount =\n columnCount ||\n Math.min(\n Math.floor((width + columnGap) / (columnWidth + columnGap)),\n maxColumnCount || Number.POSITIVE_INFINITY,\n ) ||\n 1;\n const computedColumnWidth = Math.floor(\n (width - columnGap * (computedColumnCount - 1)) / computedColumnCount,\n );\n\n const intervalTree = createIntervalTree();\n const columnHeights: number[] = new Array(computedColumnCount).fill(0);\n const items: (PositionerItem | undefined)[] = [];\n const columnItems: number[][] = new Array(computedColumnCount)\n .fill(0)\n .map(() => []);\n\n for (let i = 0; i < computedColumnCount; i++) {\n columnHeights[i] = 0;\n columnItems[i] = [];\n }\n\n return {\n columnCount: computedColumnCount,\n columnWidth: computedColumnWidth,\n set: (index: number, height = 0) => {\n let columnIndex = 0;\n\n if (linear) {\n const preferredColumn = index % computedColumnCount;\n\n let shortestHeight = columnHeights[0] ?? 0;\n let tallestHeight = shortestHeight;\n let shortestIndex = 0;\n\n for (let i = 0; i < columnHeights.length; i++) {\n const currentHeight = columnHeights[i] ?? 0;\n if (currentHeight < shortestHeight) {\n shortestHeight = currentHeight;\n shortestIndex = i;\n }\n if (currentHeight > tallestHeight) {\n tallestHeight = currentHeight;\n }\n }\n\n const preferredHeight =\n (columnHeights[preferredColumn] ?? 0) + height;\n\n const maxAllowedHeight = shortestHeight + height * 2.5;\n columnIndex =\n preferredHeight <= maxAllowedHeight\n ? preferredColumn\n : shortestIndex;\n } else {\n for (let i = 1; i < columnHeights.length; i++) {\n const currentHeight = columnHeights[i];\n const shortestHeight = columnHeights[columnIndex];\n if (\n currentHeight !== undefined &&\n shortestHeight !== undefined &&\n currentHeight < shortestHeight\n ) {\n columnIndex = i;\n }\n }\n }\n\n const columnHeight = columnHeights[columnIndex];\n if (columnHeight === undefined) return;\n\n const top = columnHeight;\n columnHeights[columnIndex] = top + height + (rowGap ?? columnGap);\n\n const columnItemsList = columnItems[columnIndex];\n if (!columnItemsList) return;\n columnItemsList.push(index);\n\n items[index] = {\n left: columnIndex * (computedColumnWidth + columnGap),\n top,\n height,\n columnIndex,\n };\n intervalTree.insert(top, top + height, index);\n },\n get: (index: number) => items[index],\n update: (updates: number[]) => {\n const columns: (number | undefined)[] = new Array(computedColumnCount);\n let i = 0;\n let j = 0;\n\n for (; i < updates.length - 1; i++) {\n const currentIndex = updates[i];\n if (typeof currentIndex !== \"number\") continue;\n\n const item = items[currentIndex];\n if (!item) continue;\n\n const nextHeight = updates[++i];\n if (typeof nextHeight !== \"number\") continue;\n\n item.height = nextHeight;\n intervalTree.remove(currentIndex);\n intervalTree.insert(item.top, item.top + item.height, currentIndex);\n columns[item.columnIndex] =\n columns[item.columnIndex] === void 0\n ? currentIndex\n : Math.min(\n currentIndex,\n columns[item.columnIndex] ?? currentIndex,\n );\n }\n\n for (i = 0; i < columns.length; i++) {\n const currentColumn = columns[i];\n if (currentColumn === void 0) continue;\n\n const itemsInColumn = columnItems[i];\n if (!itemsInColumn) continue;\n\n const startIndex = binarySearch(itemsInColumn, currentColumn);\n if (startIndex === -1) continue;\n\n const currentItemIndex = itemsInColumn[startIndex];\n if (typeof currentItemIndex !== \"number\") continue;\n\n const startItem = items[currentItemIndex];\n if (!startItem) continue;\n\n const currentHeight = columnHeights[i];\n if (typeof currentHeight !== \"number\") continue;\n\n columnHeights[i] =\n startItem.top + startItem.height + (rowGap ?? columnGap);\n\n for (j = startIndex + 1; j < itemsInColumn.length; j++) {\n const currentIndex = itemsInColumn[j];\n if (typeof currentIndex !== \"number\") continue;\n\n const item = items[currentIndex];\n if (!item) continue;\n\n const columnHeight = columnHeights[i];\n if (typeof columnHeight !== \"number\") continue;\n\n item.top = columnHeight;\n columnHeights[i] = item.top + item.height + (rowGap ?? columnGap);\n intervalTree.remove(currentIndex);\n intervalTree.insert(item.top, item.top + item.height, currentIndex);\n }\n }\n },\n range: (low, high, onItemRender) =>\n intervalTree.search(low, high, (index: number, top: number) => {\n const item = items[index];\n if (!item) return;\n onItemRender(index, item.left, top);\n }),\n estimateHeight: (itemCount, defaultItemHeight): number => {\n const tallestColumn = Math.max(0, Math.max.apply(null, columnHeights));\n\n return itemCount === intervalTree.size\n ? tallestColumn\n : tallestColumn +\n Math.ceil((itemCount - intervalTree.size) / computedColumnCount) *\n defaultItemHeight;\n },\n shortestColumn: () => {\n if (columnHeights.length > 1)\n return Math.min.apply(null, columnHeights);\n return columnHeights[0] ?? 0;\n },\n size(): number {\n return intervalTree.size;\n },\n all(): PositionerItem[] {\n return items.filter(Boolean) as PositionerItem[];\n },\n };\n }, [\n width,\n columnWidth,\n columnGap,\n rowGap,\n columnCount,\n maxColumnCount,\n linear,\n ]);\n\n const positionerRef = React.useRef(null);\n if (positionerRef.current === null) positionerRef.current = initPositioner();\n\n const prevDepsRef = React.useRef(deps);\n const opts = [\n width,\n columnWidth,\n columnGap,\n rowGap,\n columnCount,\n maxColumnCount,\n linear,\n ];\n const prevOptsRef = React.useRef(opts);\n const optsChanged = !opts.every((item, i) => prevOptsRef.current[i] === item);\n\n if (\n optsChanged ||\n !deps.every((item, i) => prevDepsRef.current[i] === item)\n ) {\n const prevPositioner = positionerRef.current;\n const positioner = initPositioner();\n prevDepsRef.current = deps;\n prevOptsRef.current = opts;\n\n if (optsChanged) {\n const cacheSize = prevPositioner.size();\n for (let index = 0; index < cacheSize; index++) {\n const pos = prevPositioner.get(index);\n positioner.set(index, pos !== void 0 ? pos.height : 0);\n }\n }\n\n positionerRef.current = positioner;\n }\n\n return positionerRef.current;\n}\n\ninterface DebouncedWindowSizeOptions {\n containerRef: React.RefObject;\n defaultWidth?: number;\n defaultHeight?: number;\n delayMs?: number;\n}\n\nfunction useDebouncedWindowSize(options: DebouncedWindowSizeOptions) {\n const {\n containerRef,\n defaultWidth = 0,\n defaultHeight = 0,\n delayMs = DEBOUNCE_DELAY,\n } = options;\n\n const getDocumentSize = React.useCallback(() => {\n if (typeof document === \"undefined\") {\n return { width: defaultWidth, height: defaultHeight };\n }\n return {\n width: document.documentElement.clientWidth,\n height: document.documentElement.clientHeight,\n };\n }, [defaultWidth, defaultHeight]);\n\n const [size, setSize] = React.useState(getDocumentSize());\n const timeoutRef = React.useRef(null);\n\n const setDebouncedSize = React.useCallback(\n (value: { width: number; height: number }) => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n timeoutRef.current = setTimeout(() => {\n setSize(value);\n }, delayMs);\n },\n [delayMs],\n );\n\n React.useEffect(() => {\n function onResize() {\n if (containerRef.current) {\n setDebouncedSize({\n width: containerRef.current.offsetWidth,\n height: document.documentElement.clientHeight,\n });\n } else {\n setDebouncedSize(getDocumentSize());\n }\n }\n\n window?.addEventListener(\"resize\", onResize, { passive: true });\n window?.addEventListener(\"orientationchange\", onResize);\n window.visualViewport?.addEventListener(\"resize\", onResize);\n\n return () => {\n window?.removeEventListener(\"resize\", onResize);\n window?.removeEventListener(\"orientationchange\", onResize);\n window.visualViewport?.removeEventListener(\"resize\", onResize);\n if (timeoutRef.current) clearTimeout(timeoutRef.current);\n };\n }, [setDebouncedSize, containerRef, getDocumentSize]);\n\n return size;\n}\n\ntype OnRafScheduleReturn = {\n (...args: T): void;\n cancel: () => void;\n};\n\nfunction onRafSchedule(\n callback: (...args: T) => void,\n): OnRafScheduleReturn {\n let lastArgs: T = [] as unknown as T;\n let frameId: number | null = null;\n\n function onCallback(...args: T) {\n lastArgs = args;\n\n if (frameId)\n frameId = requestAnimationFrame(() => {\n frameId = null;\n callback(...lastArgs);\n });\n }\n\n onCallback.cancel = () => {\n if (!frameId) return;\n cancelAnimationFrame(frameId);\n frameId = null;\n };\n\n return onCallback;\n}\n\nfunction useResizeObserver(positioner: Positioner) {\n const [, setLayoutVersion] = React.useState(0);\n\n const createResizeObserver = React.useMemo(() => {\n if (typeof window === \"undefined\") {\n return () => ({\n disconnect: () => {},\n observe: () => {},\n unobserve: () => {},\n });\n }\n\n return onDeepMemo(\n [WeakMap],\n (positioner: Positioner, onUpdate: () => void) => {\n const updates: number[] = [];\n const itemMap = new WeakMap();\n\n const update = onRafSchedule(() => {\n if (updates.length > 0) {\n positioner.update(updates);\n onUpdate();\n }\n updates.length = 0;\n });\n\n function onItemResize(target: ItemElement) {\n const height = target.offsetHeight;\n if (height > 0) {\n const index = itemMap.get(target);\n if (index !== void 0) {\n const position = positioner.get(index);\n if (position !== void 0 && height !== position.height) {\n updates.push(index, height);\n }\n }\n }\n update();\n }\n\n const scheduledItemMap = new Map<\n number,\n OnRafScheduleReturn<[ItemElement]>\n >();\n function onResizeObserver(entries: ResizeObserverEntry[]) {\n for (const entry of entries) {\n if (!entry) continue;\n const index = itemMap.get(entry.target);\n\n if (index === void 0) continue;\n let handler = scheduledItemMap.get(index);\n if (!handler) {\n handler = onRafSchedule(onItemResize);\n scheduledItemMap.set(index, handler);\n }\n handler(entry.target as ItemElement);\n }\n }\n\n const observer = new ResizeObserver(onResizeObserver);\n const disconnect = observer.disconnect.bind(observer);\n observer.disconnect = () => {\n disconnect();\n for (const [, scheduleItem] of scheduledItemMap) {\n scheduleItem.cancel();\n }\n };\n\n return observer;\n },\n );\n }, []);\n\n const resizeObserver = createResizeObserver(positioner, () =>\n setLayoutVersion((prev) => prev + 1),\n );\n\n React.useEffect(() => () => resizeObserver.disconnect(), [resizeObserver]);\n\n return resizeObserver;\n}\n\nfunction useScroller({\n offset = 0,\n fps = SCROLL_FPS,\n}: {\n offset?: number;\n fps?: number;\n} = {}): { scrollTop: number; isScrolling: boolean } {\n const [scrollY, setScrollY] = useThrottle(\n typeof globalThis.window === \"undefined\"\n ? 0\n : (globalThis.window.scrollY ?? document.documentElement.scrollTop ?? 0),\n { fps, leading: true },\n );\n\n const onScroll = React.useCallback(() => {\n setScrollY(\n globalThis.window.scrollY ?? document.documentElement.scrollTop ?? 0,\n );\n }, [setScrollY]);\n\n React.useEffect(() => {\n if (typeof globalThis.window === \"undefined\") return;\n globalThis.window.addEventListener(\"scroll\", onScroll, { passive: true });\n\n return () => globalThis.window.removeEventListener(\"scroll\", onScroll);\n }, [onScroll]);\n\n const [isScrolling, setIsScrolling] = React.useState(false);\n const hasMountedRef = React.useRef(0);\n\n React.useEffect(() => {\n if (hasMountedRef.current === 1) setIsScrolling(true);\n let didUnsubscribe = false;\n\n function requestTimeout(fn: () => void, delay: number) {\n const start = performance.now();\n const handle = {\n id: requestAnimationFrame(function tick(timestamp) {\n if (timestamp - start >= delay) {\n fn();\n } else {\n handle.id = requestAnimationFrame(tick);\n }\n }),\n };\n return handle;\n }\n\n const timeout = requestTimeout(\n () => {\n if (didUnsubscribe) return;\n setIsScrolling(false);\n },\n 40 + 1000 / fps,\n );\n hasMountedRef.current = 1;\n return () => {\n didUnsubscribe = true;\n cancelAnimationFrame(timeout.id);\n };\n }, [fps]);\n\n return { scrollTop: Math.max(0, scrollY - offset), isScrolling };\n}\n\nfunction useThrottle(\n initialState: State | (() => State),\n options: {\n fps?: number;\n leading?: boolean;\n } = {},\n): [State, React.Dispatch>] {\n const { fps = 30, leading = false } = options;\n const [state, setState] = React.useState(initialState);\n const latestSetState = React.useRef(setState);\n latestSetState.current = setState;\n\n const ms = 1000 / fps;\n const prevCountRef = React.useRef(0);\n const trailingTimeout = React.useRef | null>(\n null,\n );\n\n const clearTrailing = React.useCallback(() => {\n if (trailingTimeout.current) {\n clearTimeout(trailingTimeout.current);\n }\n }, []);\n\n React.useEffect(() => {\n return () => {\n prevCountRef.current = 0;\n clearTrailing();\n };\n }, [clearTrailing]);\n\n const throttledSetState = React.useCallback(\n (action: React.SetStateAction) => {\n const perf = typeof performance !== \"undefined\" ? performance : Date;\n const now = () => perf.now();\n const rightNow = now();\n const call = () => {\n prevCountRef.current = rightNow;\n clearTrailing();\n latestSetState.current(action);\n };\n const current = prevCountRef.current;\n\n if (leading && current === 0) {\n return call();\n }\n\n if (rightNow - current > ms) {\n if (current > 0) {\n return call();\n }\n prevCountRef.current = rightNow;\n }\n\n clearTrailing();\n trailingTimeout.current = setTimeout(() => {\n call();\n prevCountRef.current = 0;\n }, ms);\n },\n [leading, ms, clearTrailing],\n );\n\n return [state, throttledSetState];\n}\n\nconst ROOT_NAME = \"MasonryRoot\";\nconst VIEWPORT_NAME = \"MasonryViewport\";\nconst ITEM_NAME = \"MasonryItem\";\n\nconst MASONRY_ERROR = {\n [ROOT_NAME]: `\\`${ROOT_NAME}\\` components must be within \\`${ROOT_NAME}\\``,\n [VIEWPORT_NAME]: `\\`${VIEWPORT_NAME}\\` components must be within \\`${ROOT_NAME}\\``,\n [ITEM_NAME]: `\\`${ITEM_NAME}\\` must be within \\`${VIEWPORT_NAME}\\``,\n} as const;\n\ninterface DivProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {}\n\ntype RootElement = HTMLDivElement;\ntype ItemElement = HTMLDivElement;\n\ninterface MasonryContextValue {\n positioner: Positioner;\n resizeObserver?: ResizeObserver;\n columnWidth: number;\n onItemRegister: (index: number) => (node: ItemElement | null) => void;\n scrollTop: number;\n windowHeight: number;\n itemHeight: number;\n overscan: number;\n isScrolling?: boolean;\n fallback?: React.ReactNode;\n}\n\nconst MasonryContext = React.createContext(null);\n\nfunction useMasonryContext(name: keyof typeof MASONRY_ERROR) {\n const context = React.useContext(MasonryContext);\n if (!context) {\n throw new Error(MASONRY_ERROR[name]);\n }\n return context;\n}\n\ninterface MasonryProps extends DivProps {\n columnWidth?: number;\n columnCount?: number;\n maxColumnCount?: number;\n gap?: number | { column: number; row: number };\n itemHeight?: number;\n defaultWidth?: number;\n defaultHeight?: number;\n overscan?: number;\n scrollFps?: number;\n fallback?: React.ReactNode;\n linear?: boolean;\n}\n\nfunction Masonry(props: MasonryProps) {\n const {\n columnWidth = COLUMN_WIDTH,\n columnCount,\n maxColumnCount,\n gap = GAP,\n itemHeight = ITEM_HEIGHT,\n defaultWidth,\n defaultHeight,\n overscan = OVERSCAN,\n scrollFps = SCROLL_FPS,\n fallback,\n linear = false,\n children,\n style,\n ref,\n render,\n ...rootProps\n } = props;\n\n const gapValue = typeof gap === \"object\" ? gap : { column: gap, row: gap };\n const columnGap = gapValue.column;\n const rowGap = gapValue.row;\n\n const containerRef = React.useRef(null);\n const composedRef = useComposedRefs(ref, containerRef);\n\n const size = useDebouncedWindowSize({\n containerRef,\n defaultWidth,\n defaultHeight,\n delayMs: DEBOUNCE_DELAY,\n });\n\n const [containerPosition, setContainerPosition] = React.useState<{\n offset: number;\n width: number;\n }>({ offset: 0, width: 0 });\n\n useIsomorphicLayoutEffect(() => {\n if (!containerRef.current) return;\n\n let offset = 0;\n let container = containerRef.current;\n\n do {\n offset += container.offsetTop ?? 0;\n container = container.offsetParent as RootElement;\n } while (container);\n\n if (\n offset !== containerPosition.offset ||\n containerRef.current.offsetWidth !== containerPosition.width\n ) {\n setContainerPosition({\n offset,\n width: containerRef.current.offsetWidth,\n });\n }\n }, [containerPosition, size]);\n\n const positioner = usePositioner({\n width: containerPosition.width ?? size.width,\n columnWidth,\n columnGap,\n rowGap,\n columnCount,\n maxColumnCount,\n linear,\n });\n const resizeObserver = useResizeObserver(positioner);\n const { scrollTop, isScrolling } = useScroller({\n offset: containerPosition.offset,\n fps: scrollFps,\n });\n\n const itemMap = React.useRef(new WeakMap()).current;\n\n const onItemRegister = React.useCallback(\n (index: number) => (node: ItemElement | null) => {\n if (!node) return;\n\n itemMap.set(node, index);\n if (resizeObserver) {\n resizeObserver.observe(node);\n }\n if (positioner.get(index) === void 0) {\n positioner.set(index, node.offsetHeight);\n }\n },\n [itemMap, positioner, resizeObserver],\n );\n\n const contextValue = React.useMemo(\n () => ({\n positioner,\n resizeObserver,\n columnWidth: positioner.columnWidth,\n onItemRegister,\n scrollTop,\n windowHeight: size.height,\n itemHeight,\n overscan,\n fallback,\n isScrolling,\n }),\n [\n positioner,\n resizeObserver,\n onItemRegister,\n scrollTop,\n size.height,\n itemHeight,\n overscan,\n fallback,\n isScrolling,\n ],\n );\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n ref: composedRef,\n style: {\n position: \"relative\",\n width: \"100%\",\n height: \"100%\",\n ...style,\n },\n children: (\n \n {children}\n \n ),\n },\n rootProps,\n ),\n render,\n state: {\n slot: \"masonry\",\n },\n });\n}\n\ninterface MasonryItemPropsWithRef extends MasonryItemProps {\n ref: React.Ref;\n}\n\nfunction MasonryViewport(props: DivProps) {\n const { children, style, ref, ...viewportProps } = props;\n const context = useMasonryContext(VIEWPORT_NAME);\n const [layoutVersion, setLayoutVersion] = React.useState(0);\n const rafId = React.useRef(null);\n const [mounted, setMounted] = React.useState(false);\n\n useIsomorphicLayoutEffect(() => {\n setMounted(true);\n }, []);\n\n let startIndex = 0;\n let stopIndex: number | undefined;\n\n const validChildren = React.Children.toArray(children).filter(\n (child): child is React.ReactElement =>\n React.isValidElement(child) &&\n (child.type === MasonryItem || child.type === MasonryItem),\n );\n const itemCount = validChildren.length;\n\n const shortestColumnSize = context.positioner.shortestColumn();\n const measuredCount = context.positioner.size();\n const overscanPixels = context.windowHeight * context.overscan;\n const rangeStart = Math.max(0, context.scrollTop - overscanPixels / 2);\n const rangeEnd = context.scrollTop + overscanPixels;\n const layoutOutdated =\n shortestColumnSize < rangeEnd && measuredCount < itemCount;\n\n const positionedChildren: React.ReactElement[] = [];\n\n const visibleItemStyle = React.useMemo(\n (): React.CSSProperties => ({\n position: \"absolute\",\n writingMode: \"horizontal-tb\",\n visibility: \"visible\",\n width: context.columnWidth,\n transform: context.isScrolling ? \"translateZ(0)\" : undefined,\n willChange: context.isScrolling ? \"transform\" : undefined,\n }),\n [context.columnWidth, context.isScrolling],\n );\n\n const hiddenItemStyle = React.useMemo(\n (): React.CSSProperties => ({\n position: \"absolute\",\n writingMode: \"horizontal-tb\",\n visibility: \"hidden\",\n width: context.columnWidth,\n zIndex: -1000,\n }),\n [context.columnWidth],\n );\n\n context.positioner.range(rangeStart, rangeEnd, (index, left, top) => {\n const child = validChildren[index];\n if (!child) return;\n\n const itemStyle = {\n ...visibleItemStyle,\n top,\n left,\n ...child.props.style,\n };\n\n positionedChildren.push(\n React.cloneElement(child, {\n key: child.key ?? index,\n ref: context.onItemRegister(index),\n style: itemStyle,\n }),\n );\n\n if (stopIndex === undefined) {\n startIndex = index;\n stopIndex = index;\n } else {\n startIndex = Math.min(startIndex, index);\n stopIndex = Math.max(stopIndex, index);\n }\n });\n\n if (layoutOutdated && mounted) {\n const batchSize = Math.min(\n itemCount - measuredCount,\n Math.ceil(\n ((context.scrollTop + overscanPixels - shortestColumnSize) /\n context.itemHeight) *\n context.positioner.columnCount,\n ),\n );\n\n for (\n let index = measuredCount;\n index < measuredCount + batchSize;\n index++\n ) {\n const child = validChildren[index];\n if (!child) continue;\n\n const itemStyle = {\n ...hiddenItemStyle,\n ...child.props.style,\n };\n\n positionedChildren.push(\n React.cloneElement(child, {\n key: child.key ?? index,\n ref: context.onItemRegister(index),\n style: itemStyle,\n }),\n );\n }\n }\n\n React.useEffect(() => {\n if (layoutOutdated && mounted) {\n if (rafId.current) {\n cancelAnimationFrame(rafId.current);\n }\n rafId.current = requestAnimationFrame(() => {\n setLayoutVersion((v) => v + 1);\n });\n }\n return () => {\n if (rafId.current) {\n cancelAnimationFrame(rafId.current);\n }\n };\n }, [layoutOutdated, mounted]);\n\n const estimatedHeight = React.useMemo(() => {\n const measuredHeight = context.positioner.estimateHeight(\n measuredCount,\n context.itemHeight,\n );\n if (measuredCount === itemCount) {\n return measuredHeight;\n }\n const remainingItems = itemCount - measuredCount;\n const estimatedRemainingHeight = Math.ceil(\n (remainingItems / context.positioner.columnCount) * context.itemHeight,\n );\n return measuredHeight + estimatedRemainingHeight;\n }, [context.positioner, context.itemHeight, measuredCount, itemCount]);\n\n const containerStyle = React.useMemo(\n () => ({\n position: \"relative\" as const,\n width: \"100%\",\n maxWidth: \"100%\",\n height: Math.ceil(estimatedHeight),\n maxHeight: Math.ceil(estimatedHeight),\n willChange: context.isScrolling ? \"contents\" : undefined,\n pointerEvents: context.isScrolling ? (\"none\" as const) : undefined,\n ...style,\n }),\n [context.isScrolling, estimatedHeight, style],\n );\n\n if (!mounted && context.fallback) {\n return context.fallback;\n }\n\n return (\n \n {positionedChildren}\n \n );\n}\n\ninterface MasonryItemProps extends DivProps {}\n\nfunction MasonryItem(props: MasonryItemProps) {\n const { ref, render, ...itemProps } = props;\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">({ ref }, itemProps),\n render,\n state: {\n slot: \"masonry-item\",\n },\n });\n}\n\nexport { Masonry, MasonryItem, type MasonryProps };\n", "target": "" }, { "path": "lib/compose-refs.ts", "type": "registry:lib", "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef = React.Ref | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef(ref: PossibleRef, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n }\n\n if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs(...refs: PossibleRef[]): React.RefCallback {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup === \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup === \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs(...refs: PossibleRef[]): React.RefCallback {\n // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", "target": "" } ], "registryDependencies": [ "@diceui/use-isomorphic-layout-effect" ], "dependencies": [ "@base-ui/react" ] }