{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "Cubes-JS-TW",
	"title": "Cubes",
	"description": "3D rotating cube cluster. Supports auto-rotation or hover interaction.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "Cubes/Cubes.jsx",
			"content": "import { useCallback, useEffect, useRef } from 'react';\nimport gsap from 'gsap';\n\nconst Cubes = ({\n  gridSize = 10,\n  cubeSize,\n  maxAngle = 45,\n  radius = 3,\n  easing = 'power3.out',\n  duration = { enter: 0.3, leave: 0.6 },\n  cellGap,\n  borderStyle = '1px solid #fff',\n  faceColor = '#120F17',\n  shadow = false,\n  autoAnimate = true,\n  rippleOnClick = true,\n  rippleColor = '#fff',\n  rippleSpeed = 2\n}) => {\n  const sceneRef = useRef(null);\n  const rafRef = useRef(null);\n  const idleTimerRef = useRef(null);\n  const userActiveRef = useRef(false);\n  const simPosRef = useRef({ x: 0, y: 0 });\n  const simTargetRef = useRef({ x: 0, y: 0 });\n  const simRAFRef = useRef(null);\n\n  const colGap = typeof cellGap === 'number' ? `${cellGap}px` : cellGap?.col !== undefined ? `${cellGap.col}px` : '5%';\n  const rowGap = typeof cellGap === 'number' ? `${cellGap}px` : cellGap?.row !== undefined ? `${cellGap.row}px` : '5%';\n\n  const enterDur = duration.enter;\n  const leaveDur = duration.leave;\n\n  const tiltAt = useCallback(\n    (rowCenter, colCenter) => {\n      if (!sceneRef.current) return;\n      sceneRef.current.querySelectorAll('.cube').forEach(cube => {\n        const r = +cube.dataset.row;\n        const c = +cube.dataset.col;\n        const dist = Math.hypot(r - rowCenter, c - colCenter);\n        if (dist <= radius) {\n          const pct = 1 - dist / radius;\n          const angle = pct * maxAngle;\n          gsap.to(cube, {\n            duration: enterDur,\n            ease: easing,\n            overwrite: true,\n            rotateX: -angle,\n            rotateY: angle\n          });\n        } else {\n          gsap.to(cube, {\n            duration: leaveDur,\n            ease: 'power3.out',\n            overwrite: true,\n            rotateX: 0,\n            rotateY: 0\n          });\n        }\n      });\n    },\n    [radius, maxAngle, enterDur, leaveDur, easing]\n  );\n\n  const onPointerMove = useCallback(\n    e => {\n      userActiveRef.current = true;\n      if (idleTimerRef.current) clearTimeout(idleTimerRef.current);\n\n      const rect = sceneRef.current.getBoundingClientRect();\n      const cellW = rect.width / gridSize;\n      const cellH = rect.height / gridSize;\n      const colCenter = (e.clientX - rect.left) / cellW;\n      const rowCenter = (e.clientY - rect.top) / cellH;\n\n      if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      rafRef.current = requestAnimationFrame(() => tiltAt(rowCenter, colCenter));\n\n      idleTimerRef.current = setTimeout(() => {\n        userActiveRef.current = false;\n      }, 3000);\n    },\n    [gridSize, tiltAt]\n  );\n\n  const resetAll = useCallback(() => {\n    if (!sceneRef.current) return;\n    sceneRef.current.querySelectorAll('.cube').forEach(cube =>\n      gsap.to(cube, {\n        duration: leaveDur,\n        rotateX: 0,\n        rotateY: 0,\n        ease: 'power3.out'\n      })\n    );\n  }, [leaveDur]);\n\n  const onTouchMove = useCallback(\n    e => {\n      e.preventDefault();\n      userActiveRef.current = true;\n      if (idleTimerRef.current) clearTimeout(idleTimerRef.current);\n\n      const rect = sceneRef.current.getBoundingClientRect();\n      const cellW = rect.width / gridSize;\n      const cellH = rect.height / gridSize;\n\n      const touch = e.touches[0];\n      const colCenter = (touch.clientX - rect.left) / cellW;\n      const rowCenter = (touch.clientY - rect.top) / cellH;\n\n      if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      rafRef.current = requestAnimationFrame(() => tiltAt(rowCenter, colCenter));\n\n      idleTimerRef.current = setTimeout(() => {\n        userActiveRef.current = false;\n      }, 3000);\n    },\n    [gridSize, tiltAt]\n  );\n\n  const onTouchStart = useCallback(() => {\n    userActiveRef.current = true;\n  }, []);\n\n  const onTouchEnd = useCallback(() => {\n    if (!sceneRef.current) return;\n    resetAll();\n  }, [resetAll]);\n\n  const onClick = useCallback(\n    e => {\n      if (!rippleOnClick || !sceneRef.current) return;\n      const rect = sceneRef.current.getBoundingClientRect();\n      const cellW = rect.width / gridSize;\n      const cellH = rect.height / gridSize;\n\n      const clientX = e.clientX || (e.touches && e.touches[0].clientX);\n      const clientY = e.clientY || (e.touches && e.touches[0].clientY);\n\n      const colHit = Math.floor((clientX - rect.left) / cellW);\n      const rowHit = Math.floor((clientY - rect.top) / cellH);\n\n      const baseRingDelay = 0.15;\n      const baseAnimDur = 0.3;\n      const baseHold = 0.6;\n\n      const spreadDelay = baseRingDelay / rippleSpeed;\n      const animDuration = baseAnimDur / rippleSpeed;\n      const holdTime = baseHold / rippleSpeed;\n\n      const rings = {};\n      sceneRef.current.querySelectorAll('.cube').forEach(cube => {\n        const r = +cube.dataset.row;\n        const c = +cube.dataset.col;\n        const dist = Math.hypot(r - rowHit, c - colHit);\n        const ring = Math.round(dist);\n        if (!rings[ring]) rings[ring] = [];\n        rings[ring].push(cube);\n      });\n\n      Object.keys(rings)\n        .map(Number)\n        .sort((a, b) => a - b)\n        .forEach(ring => {\n          const delay = ring * spreadDelay;\n          const faces = rings[ring].flatMap(cube => Array.from(cube.querySelectorAll('.cube-face')));\n\n          gsap.to(faces, {\n            backgroundColor: rippleColor,\n            duration: animDuration,\n            delay,\n            ease: 'power3.out'\n          });\n          gsap.to(faces, {\n            backgroundColor: faceColor,\n            duration: animDuration,\n            delay: delay + animDuration + holdTime,\n            ease: 'power3.out'\n          });\n        });\n    },\n    [rippleOnClick, gridSize, faceColor, rippleColor, rippleSpeed]\n  );\n\n  useEffect(() => {\n    if (!autoAnimate || !sceneRef.current) return;\n    simPosRef.current = {\n      x: Math.random() * gridSize,\n      y: Math.random() * gridSize\n    };\n    simTargetRef.current = {\n      x: Math.random() * gridSize,\n      y: Math.random() * gridSize\n    };\n    const speed = 0.02;\n    const loop = () => {\n      if (!userActiveRef.current) {\n        const pos = simPosRef.current;\n        const tgt = simTargetRef.current;\n        pos.x += (tgt.x - pos.x) * speed;\n        pos.y += (tgt.y - pos.y) * speed;\n        tiltAt(pos.y, pos.x);\n        if (Math.hypot(pos.x - tgt.x, pos.y - tgt.y) < 0.1) {\n          simTargetRef.current = {\n            x: Math.random() * gridSize,\n            y: Math.random() * gridSize\n          };\n        }\n      }\n      simRAFRef.current = requestAnimationFrame(loop);\n    };\n    simRAFRef.current = requestAnimationFrame(loop);\n    return () => {\n      if (simRAFRef.current != null) cancelAnimationFrame(simRAFRef.current);\n    };\n  }, [autoAnimate, gridSize, tiltAt]);\n\n  useEffect(() => {\n    const el = sceneRef.current;\n    if (!el) return;\n\n    el.addEventListener('pointermove', onPointerMove);\n    el.addEventListener('pointerleave', resetAll);\n    el.addEventListener('click', onClick);\n\n    el.addEventListener('touchmove', onTouchMove, { passive: false });\n    el.addEventListener('touchstart', onTouchStart, { passive: true });\n    el.addEventListener('touchend', onTouchEnd, { passive: true });\n\n    return () => {\n      el.removeEventListener('pointermove', onPointerMove);\n      el.removeEventListener('pointerleave', resetAll);\n      el.removeEventListener('click', onClick);\n\n      el.removeEventListener('touchmove', onTouchMove);\n      el.removeEventListener('touchstart', onTouchStart);\n      el.removeEventListener('touchend', onTouchEnd);\n\n      if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n      if (idleTimerRef.current) clearTimeout(idleTimerRef.current);\n    };\n  }, [onPointerMove, resetAll, onClick, onTouchMove, onTouchStart, onTouchEnd]);\n\n  const cells = Array.from({ length: gridSize });\n  const sceneStyle = {\n    gridTemplateColumns: cubeSize ? `repeat(${gridSize}, ${cubeSize}px)` : `repeat(${gridSize}, 1fr)`,\n    gridTemplateRows: cubeSize ? `repeat(${gridSize}, ${cubeSize}px)` : `repeat(${gridSize}, 1fr)`,\n    columnGap: colGap,\n    rowGap: rowGap,\n    perspective: '99999999px',\n    gridAutoRows: '1fr'\n  };\n  const wrapperStyle = {\n    '--cube-face-border': borderStyle,\n    '--cube-face-bg': faceColor,\n    '--cube-face-shadow': shadow === true ? '0 0 6px rgba(0,0,0,.5)' : shadow || 'none',\n    ...(cubeSize\n      ? {\n          width: `${gridSize * cubeSize}px`,\n          height: `${gridSize * cubeSize}px`\n        }\n      : {})\n  };\n\n  return (\n    <div className=\"relative w-1/2 max-md:w-11/12 aspect-square\" style={wrapperStyle}>\n      <div ref={sceneRef} className=\"grid w-full h-full\" style={sceneStyle}>\n        {cells.map((_, r) =>\n          cells.map((__, c) => (\n            <div\n              key={`${r}-${c}`}\n              className=\"cube relative w-full h-full aspect-square [transform-style:preserve-3d]\"\n              data-row={r}\n              data-col={c}\n            >\n              <span className=\"absolute pointer-events-none -inset-9\" />\n\n              <div\n                className=\"cube-face absolute inset-0 flex items-center justify-center\"\n                style={{\n                  background: 'var(--cube-face-bg)',\n                  border: 'var(--cube-face-border)',\n                  boxShadow: 'var(--cube-face-shadow)',\n                  transform: 'translateY(-50%) rotateX(90deg)'\n                }}\n              />\n              <div\n                className=\"cube-face absolute inset-0 flex items-center justify-center\"\n                style={{\n                  background: 'var(--cube-face-bg)',\n                  border: 'var(--cube-face-border)',\n                  boxShadow: 'var(--cube-face-shadow)',\n                  transform: 'translateY(50%) rotateX(-90deg)'\n                }}\n              />\n              <div\n                className=\"cube-face absolute inset-0 flex items-center justify-center\"\n                style={{\n                  background: 'var(--cube-face-bg)',\n                  border: 'var(--cube-face-border)',\n                  boxShadow: 'var(--cube-face-shadow)',\n                  transform: 'translateX(-50%) rotateY(-90deg)'\n                }}\n              />\n              <div\n                className=\"cube-face absolute inset-0 flex items-center justify-center\"\n                style={{\n                  background: 'var(--cube-face-bg)',\n                  border: 'var(--cube-face-border)',\n                  boxShadow: 'var(--cube-face-shadow)',\n                  transform: 'translateX(50%) rotateY(90deg)'\n                }}\n              />\n              <div\n                className=\"cube-face absolute inset-0 flex items-center justify-center\"\n                style={{\n                  background: 'var(--cube-face-bg)',\n                  border: 'var(--cube-face-border)',\n                  boxShadow: 'var(--cube-face-shadow)',\n                  transform: 'rotateY(-90deg) translateX(50%) rotateY(90deg)'\n                }}\n              />\n              <div\n                className=\"cube-face absolute inset-0 flex items-center justify-center\"\n                style={{\n                  background: 'var(--cube-face-bg)',\n                  border: 'var(--cube-face-border)',\n                  boxShadow: 'var(--cube-face-shadow)',\n                  transform: 'rotateY(90deg) translateX(-50%) rotateY(-90deg)'\n                }}\n              />\n            </div>\n          ))\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default Cubes;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0"
	]
}