{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "DriftWall-JS-TW",
	"title": "DriftWall",
	"description": "An endless perspective wall of tiles drifting past, lifting on hover.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "DriftWall/DriftWall.jsx",
			"content": "import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\n\nconst DEFAULT_ITEMS = Array.from({ length: 15 }, (_, i) => {\n  const ids = [1015, 1025, 1039, 1043, 1044, 1050, 1062, 1069, 1074, 1080, 1084, 106, 110, 133, 164];\n  return {\n    image: `https://picsum.photos/id/${ids[i % ids.length]}/600/400`,\n    title: `Tile ${i + 1}`,\n    href: undefined\n  };\n});\n\nconst cx = (...parts) => parts.filter(Boolean).join(' ');\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nconst columnFactor = (index, variance) => {\n  const pseudo = ((index * 0.6180339887 + 0.35) % 1) * 2 - 1;\n  return 1 + variance * pseudo;\n};\n\nconst DriftWall = ({\n  items = DEFAULT_ITEMS,\n  columns = 5,\n  tileWidth = 200,\n  tileHeight = 132,\n  gap = 18,\n  radius = 14,\n  tilt = 16,\n  turn = -14,\n  roll = 0,\n  perspective = 1200,\n  depth = 120,\n  speed = 42,\n  direction = 'up',\n  variance = 0.45,\n  parallax = 0.6,\n  pauseOnHover = false,\n  lift = 64,\n  fade = 0.6,\n  dim = 0.55,\n  grayscale = false,\n  overlayColor = '#060010',\n  className = '',\n  style\n}) => {\n  const containerRef = useRef(null);\n  const planeRef = useRef(null);\n  const trackRefs = useRef([]);\n  const rafRef = useRef(null);\n\n  const offsetsRef = useRef([]);\n  const velocitiesRef = useRef([]);\n  const hoveredColRef = useRef(-1);\n  const wallHoveredRef = useRef(false);\n  const pointerRef = useRef({ x: 0, y: 0 });\n  const pointerDampedRef = useRef({ x: 0, y: 0 });\n  const lastTsRef = useRef(null);\n\n  const [containerHeight, setContainerHeight] = useState(600);\n  const [activeId, setActiveId] = useState(null);\n  const activeIdRef = useRef(null);\n  const [reduced, setReduced] = useState(false);\n\n  useEffect(() => {\n    setReduced(prefersReducedMotion());\n    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n    const onChange = e => setReduced(e.matches);\n    mq.addEventListener('change', onChange);\n    return () => mq.removeEventListener('change', onChange);\n  }, []);\n\n  const columnItems = useMemo(() => {\n    const cols = Array.from({ length: columns }, () => []);\n    items.forEach((item, i) => cols[i % columns].push(item));\n    return cols.map(col => (col.length ? col : items.slice(0, 1)));\n  }, [items, columns]);\n\n  const columnMeta = useMemo(() => {\n    const unit = tileHeight + gap;\n    return columnItems.map(col => {\n      const copyHeight = Math.max(unit, col.length * unit);\n      const copies = Math.max(2, Math.ceil((containerHeight * 1.6) / copyHeight) + 1);\n      return { copyHeight, copies };\n    });\n  }, [columnItems, tileHeight, gap, containerHeight]);\n\n  useLayoutEffect(() => {\n    if (!containerRef.current) return;\n    const ro = new ResizeObserver(([entry]) => {\n      setContainerHeight(entry.contentRect.height || 600);\n    });\n    ro.observe(containerRef.current);\n    return () => ro.disconnect();\n  }, []);\n\n  const baseVelocities = useMemo(() => {\n    const dirSign = direction === 'up' ? 1 : -1;\n    return columnItems.map((_, c) => {\n      const altSign = c % 2 === 0 ? 1 : -1;\n      return speed * columnFactor(c, variance) * dirSign * altSign;\n    });\n  }, [columnItems, speed, direction, variance]);\n\n  useEffect(() => {\n    offsetsRef.current = columnMeta.map((meta, c) => meta.copyHeight * ((c * 0.37) % 1));\n    velocitiesRef.current = columnItems.map(() => 0);\n  }, [columnMeta, columnItems]);\n\n  const applyPlaneTransform = useCallback(\n    (px, py) => {\n      const plane = planeRef.current;\n      if (!plane) return;\n      plane.style.transform =\n        `translate(-50%, -50%) scale(1.18) ` +\n        `rotateX(${tilt + py}deg) rotateY(${turn + px}deg) rotateZ(${roll}deg) ` +\n        `translateZ(${-depth}px)`;\n    },\n    [tilt, turn, roll, depth]\n  );\n\n  useEffect(() => {\n    const animate = ts => {\n      if (lastTsRef.current === null) lastTsRef.current = ts;\n      const dt = Math.min(0.05, Math.max(0, ts - lastTsRef.current) / 1000);\n      lastTsRef.current = ts;\n\n      const maxTilt = parallax * 8;\n      const targetX = pointerRef.current.x * maxTilt;\n      const targetY = -pointerRef.current.y * maxTilt;\n      const damp = 1 - Math.exp(-dt / 0.12);\n      pointerDampedRef.current.x += (targetX - pointerDampedRef.current.x) * damp;\n      pointerDampedRef.current.y += (targetY - pointerDampedRef.current.y) * damp;\n      applyPlaneTransform(pointerDampedRef.current.x, pointerDampedRef.current.y);\n\n      if (!reduced) {\n        for (let c = 0; c < trackRefs.current.length; c++) {\n          const meta = columnMeta[c];\n          if (!meta) continue;\n          const paused = wallHoveredRef.current && pauseOnHover;\n          const factor = paused || hoveredColRef.current === c ? 0 : 1;\n          const target = baseVelocities[c] * factor;\n\n          const ease = 1 - Math.exp(-dt / (target === 0 ? 0.16 : 0.28));\n          velocitiesRef.current[c] += (target - velocitiesRef.current[c]) * ease;\n          let next = (offsetsRef.current[c] ?? 0) + velocitiesRef.current[c] * dt;\n          next = ((next % meta.copyHeight) + meta.copyHeight) % meta.copyHeight;\n          offsetsRef.current[c] = next;\n\n          const el = trackRefs.current[c];\n          if (el) el.style.transform = `translate3d(0, ${-next}px, 0)`;\n        }\n      } else {\n        for (let c = 0; c < trackRefs.current.length; c++) {\n          const el = trackRefs.current[c];\n          const meta = columnMeta[c];\n          if (el && meta) el.style.transform = `translate3d(0, ${-(offsetsRef.current[c] ?? 0)}px, 0)`;\n        }\n      }\n\n      rafRef.current = requestAnimationFrame(animate);\n    };\n\n    rafRef.current = requestAnimationFrame(animate);\n    return () => {\n      if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      rafRef.current = null;\n      lastTsRef.current = null;\n    };\n  }, [baseVelocities, columnMeta, pauseOnHover, parallax, reduced, applyPlaneTransform]);\n\n  const activate = useCallback((id, index) => {\n    activeIdRef.current = id;\n    hoveredColRef.current = index;\n    setActiveId(id);\n  }, []);\n  const release = useCallback(() => {\n    activeIdRef.current = null;\n    hoveredColRef.current = -1;\n    setActiveId(null);\n  }, []);\n\n  const handlePointerMove = useCallback(\n    e => {\n      const rect = containerRef.current?.getBoundingClientRect();\n      if (!rect) return;\n      if (parallax > 0 && !reduced) {\n        pointerRef.current = {\n          x: (e.clientX - rect.left) / rect.width - 0.5,\n          y: (e.clientY - rect.top) / rect.height - 0.5\n        };\n      }\n      const hit = document.elementFromPoint(e.clientX, e.clientY);\n      const tile = hit && hit.closest ? hit.closest('[data-tile-id]') : null;\n      if (!tile) return;\n      const id = tile.dataset.tileId;\n      if (id === activeIdRef.current) return;\n      activeIdRef.current = id;\n      hoveredColRef.current = Number(tile.dataset.col);\n      setActiveId(id);\n    },\n    [parallax, reduced]\n  );\n\n  const handlePointerLeaveWall = useCallback(() => {\n    wallHoveredRef.current = false;\n    pointerRef.current = { x: 0, y: 0 };\n    release();\n  }, [release]);\n\n  const maskStyle =\n    'radial-gradient(ellipse 78% 82% at 50% 46%, #000 var(--dw-edge), transparent 100%), ' +\n    'linear-gradient(to top, #000 var(--dw-edge), transparent 100%)';\n\n  const cssVars = useMemo(\n    () => ({\n      '--dw-tile-w': `${tileWidth}px`,\n      '--dw-tile-h': `${tileHeight}px`,\n      '--dw-gap': `${gap}px`,\n      '--dw-radius': `${radius}px`,\n      '--dw-lift': `${lift}px`,\n      '--dw-dim': dim,\n      '--dw-gray': grayscale ? 1 : 0,\n      '--dw-overlay': overlayColor,\n      '--dw-edge': `${Math.max(0, (1 - fade) * 100)}%`,\n      perspective: `${perspective}px`,\n      perspectiveOrigin: '50% 50%',\n      WebkitMaskImage: maskStyle,\n      maskImage: maskStyle,\n      WebkitMaskComposite: 'source-in',\n      maskComposite: 'intersect',\n      ...style\n    }),\n    [tileWidth, tileHeight, gap, radius, lift, dim, grayscale, overlayColor, fade, perspective, maskStyle, style]\n  );\n\n  const tileClass = cx(\n    'group/tile relative block flex-none cursor-pointer outline-none',\n    'w-full h-[calc(var(--dw-tile-h)+var(--dw-gap))] [transform-style:preserve-3d]'\n  );\n  const innerClass = cx(\n    'pointer-events-none absolute inset-[calc(var(--dw-gap)/2)] block overflow-hidden bg-[#0b0b12]',\n    'rounded-[var(--dw-radius)] opacity-[var(--dw-dim)] [transform:translateZ(0)]',\n    'transition-[transform,opacity,box-shadow] duration-[420ms] ease-[cubic-bezier(0.22,1,0.36,1)]',\n    'group-[.is-active]/tile:opacity-100 group-[.is-active]/tile:[transform:translateZ(var(--dw-lift))]',\n    'group-[.is-active]/tile:shadow-[0_24px_60px_-18px_rgba(0,0,0,0.7)]',\n    'group-focus-visible/tile:opacity-100 group-focus-visible/tile:[transform:translateZ(var(--dw-lift))]',\n    'group-focus-visible/tile:shadow-[0_24px_60px_-18px_rgba(0,0,0,0.7),0_0_0_2px_rgba(255,255,255,0.9)]'\n  );\n  const imgClass = cx(\n    'block h-full w-full select-none object-cover',\n    '[filter:grayscale(var(--dw-gray))_saturate(0.92)]',\n    'transition-[filter] duration-[420ms] ease-[cubic-bezier(0.22,1,0.36,1)]',\n    'group-[.is-active]/tile:[filter:grayscale(0)_saturate(1.05)] group-focus-visible/tile:[filter:grayscale(0)_saturate(1.05)]'\n  );\n  const overlayClass = cx(\n    'pointer-events-none absolute inset-0 bg-[var(--dw-overlay)] opacity-[0.42]',\n    'transition-opacity duration-[420ms] ease-[cubic-bezier(0.22,1,0.36,1)]',\n    'group-[.is-active]/tile:opacity-0 group-focus-visible/tile:opacity-0'\n  );\n\n  const renderTile = (item, id, colIndex) => {\n    const inner = (\n      <span className={innerClass}>\n        <img\n          src={item.image}\n          alt={item.title ?? ''}\n          loading=\"lazy\"\n          decoding=\"async\"\n          draggable={false}\n          className={imgClass}\n        />\n        <span className={overlayClass} aria-hidden=\"true\" />\n      </span>\n    );\n    const commonProps = {\n      className: cx(tileClass, activeId === id && 'is-active'),\n      'data-tile-id': id,\n      'data-col': colIndex,\n      onFocus: () => activate(id, colIndex),\n      onBlur: release\n    };\n    if (item.href) {\n      return (\n        <a key={id} href={item.href} target=\"_blank\" rel=\"noreferrer noopener\" {...commonProps}>\n          {inner}\n        </a>\n      );\n    }\n    return (\n      <div key={id} tabIndex={0} role=\"button\" aria-label={item.title ?? 'tile'} {...commonProps}>\n        {inner}\n      </div>\n    );\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      className={cx('relative h-full w-full overflow-hidden', className)}\n      style={cssVars}\n      onPointerMove={handlePointerMove}\n      onPointerEnter={() => {\n        wallHoveredRef.current = true;\n      }}\n      onPointerLeave={handlePointerLeaveWall}\n      role=\"group\"\n      aria-label=\"Drifting wall of tiles\"\n    >\n      <div\n        ref={planeRef}\n        className=\"absolute left-1/2 top-1/2 flex cursor-pointer flex-row [transform-style:preserve-3d] [transform-origin:50%_50%] will-change-transform\"\n      >\n        {columnItems.map((col, c) => {\n          const meta = columnMeta[c];\n          const copies = Array.from({ length: meta.copies });\n          return (\n            <div\n              className=\"relative w-[calc(var(--dw-tile-w)+var(--dw-gap))] [transform-style:preserve-3d]\"\n              key={`col-${c}`}\n            >\n              <div\n                className=\"flex flex-col [transform-style:preserve-3d] will-change-transform\"\n                ref={el => (trackRefs.current[c] = el)}\n              >\n                {copies.map((_, copyIndex) =>\n                  col.map((item, itemIndex) => renderTile(item, `${c}-${copyIndex}-${itemIndex}`, c))\n                )}\n              </div>\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n\nexport default DriftWall;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}