{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "ShapeGrid-TS-CSS",
	"title": "ShapeGrid",
	"description": "Animated grid with shape variants (square, hexagon, circle, triangle) + direction customization.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "ShapeGrid.css",
			"target": "@components/ShapeGrid.css",
			"content": ".shapegrid-canvas {\n  width: 100%;\n  height: 100%;\n  border: none;\n  display: block;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "ShapeGrid.tsx",
			"content": "import React, { useRef, useEffect } from 'react';\nimport './ShapeGrid.css';\n\ntype CanvasStrokeStyle = string | CanvasGradient | CanvasPattern;\n\ninterface GridOffset {\n  x: number;\n  y: number;\n}\n\ninterface ShapeGridProps {\n  direction?: 'diagonal' | 'up' | 'right' | 'down' | 'left';\n  speed?: number;\n  borderColor?: CanvasStrokeStyle;\n  squareSize?: number;\n  hoverFillColor?: CanvasStrokeStyle;\n  shape?: 'square' | 'hexagon' | 'circle' | 'triangle';\n  hoverTrailAmount?: number;\n}\n\nconst ShapeGrid: React.FC<ShapeGridProps> = ({\n  direction = 'right',\n  speed = 1,\n  borderColor = '#999',\n  squareSize = 40,\n  hoverFillColor = '#222',\n  shape = 'square',\n  hoverTrailAmount = 0\n}) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const requestRef = useRef<number | null>(null);\n  const numSquaresX = useRef<number>(0);\n  const numSquaresY = useRef<number>(0);\n  const gridOffset = useRef<GridOffset>({ x: 0, y: 0 });\n  const hoveredSquareRef = useRef<GridOffset | null>(null);\n  const trailCells = useRef<GridOffset[]>([]);\n  const cellOpacities = useRef<Map<string, number>>(new Map());\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext('2d');\n\n    const isHex = shape === 'hexagon';\n    const isTri = shape === 'triangle';\n    const hexHoriz = squareSize * 1.5;\n    const hexVert = squareSize * Math.sqrt(3);\n\n    const resizeCanvas = () => {\n      canvas.width = canvas.offsetWidth;\n      canvas.height = canvas.offsetHeight;\n      numSquaresX.current = Math.ceil(canvas.width / squareSize) + 1;\n      numSquaresY.current = Math.ceil(canvas.height / squareSize) + 1;\n    };\n\n    window.addEventListener('resize', resizeCanvas);\n    resizeCanvas();\n\n    const drawHex = (cx: number, cy: number, size: number) => {\n      if (!ctx) return;\n      ctx.beginPath();\n      for (let i = 0; i < 6; i++) {\n        const angle = (Math.PI / 3) * i;\n        const vx = cx + size * Math.cos(angle);\n        const vy = cy + size * Math.sin(angle);\n        if (i === 0) ctx.moveTo(vx, vy);\n        else ctx.lineTo(vx, vy);\n      }\n      ctx.closePath();\n    };\n\n    const drawCircle = (cx: number, cy: number, size: number) => {\n      if (!ctx) return;\n      ctx.beginPath();\n      ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);\n      ctx.closePath();\n    };\n\n    const drawTriangle = (cx: number, cy: number, size: number, flip: boolean) => {\n      if (!ctx) return;\n      ctx.beginPath();\n      if (flip) {\n        ctx.moveTo(cx, cy + size / 2);\n        ctx.lineTo(cx + size / 2, cy - size / 2);\n        ctx.lineTo(cx - size / 2, cy - size / 2);\n      } else {\n        ctx.moveTo(cx, cy - size / 2);\n        ctx.lineTo(cx + size / 2, cy + size / 2);\n        ctx.lineTo(cx - size / 2, cy + size / 2);\n      }\n      ctx.closePath();\n    };\n\n    const drawGrid = () => {\n      if (!ctx) return;\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n      if (isHex) {\n        const colShift = Math.floor(gridOffset.current.x / hexHoriz);\n        const offsetX = ((gridOffset.current.x % hexHoriz) + hexHoriz) % hexHoriz;\n        const offsetY = ((gridOffset.current.y % hexVert) + hexVert) % hexVert;\n\n        const cols = Math.ceil(canvas.width / hexHoriz) + 3;\n        const rows = Math.ceil(canvas.height / hexVert) + 3;\n\n        for (let col = -2; col < cols; col++) {\n          for (let row = -2; row < rows; row++) {\n            const cx = col * hexHoriz + offsetX;\n            const cy = row * hexVert + ((col + colShift) % 2 !== 0 ? hexVert / 2 : 0) + offsetY;\n\n            const cellKey = `${col},${row}`;\n            const alpha = cellOpacities.current.get(cellKey);\n            if (alpha) {\n              ctx.globalAlpha = alpha;\n              drawHex(cx, cy, squareSize);\n              ctx.fillStyle = hoverFillColor;\n              ctx.fill();\n              ctx.globalAlpha = 1;\n            }\n\n            drawHex(cx, cy, squareSize);\n            ctx.strokeStyle = borderColor;\n            ctx.stroke();\n          }\n        }\n      } else if (isTri) {\n        const halfW = squareSize / 2;\n        const colShift = Math.floor(gridOffset.current.x / halfW);\n        const rowShift = Math.floor(gridOffset.current.y / squareSize);\n        const offsetX = ((gridOffset.current.x % halfW) + halfW) % halfW;\n        const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize;\n\n        const cols = Math.ceil(canvas.width / halfW) + 4;\n        const rows = Math.ceil(canvas.height / squareSize) + 4;\n\n        for (let col = -2; col < cols; col++) {\n          for (let row = -2; row < rows; row++) {\n            const cx = col * halfW + offsetX;\n            const cy = row * squareSize + squareSize / 2 + offsetY;\n            const flip = ((col + colShift + row + rowShift) % 2 + 2) % 2 !== 0;\n\n            const cellKey = `${col},${row}`;\n            const alpha = cellOpacities.current.get(cellKey);\n            if (alpha) {\n              ctx.globalAlpha = alpha;\n              drawTriangle(cx, cy, squareSize, flip);\n              ctx.fillStyle = hoverFillColor;\n              ctx.fill();\n              ctx.globalAlpha = 1;\n            }\n\n            drawTriangle(cx, cy, squareSize, flip);\n            ctx.strokeStyle = borderColor;\n            ctx.stroke();\n          }\n        }\n      } else if (shape === 'circle') {\n        const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize;\n        const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize;\n\n        const cols = Math.ceil(canvas.width / squareSize) + 3;\n        const rows = Math.ceil(canvas.height / squareSize) + 3;\n\n        for (let col = -2; col < cols; col++) {\n          for (let row = -2; row < rows; row++) {\n            const cx = col * squareSize + squareSize / 2 + offsetX;\n            const cy = row * squareSize + squareSize / 2 + offsetY;\n\n            const cellKey = `${col},${row}`;\n            const alpha = cellOpacities.current.get(cellKey);\n            if (alpha) {\n              ctx.globalAlpha = alpha;\n              drawCircle(cx, cy, squareSize);\n              ctx.fillStyle = hoverFillColor;\n              ctx.fill();\n              ctx.globalAlpha = 1;\n            }\n\n            drawCircle(cx, cy, squareSize);\n            ctx.strokeStyle = borderColor;\n            ctx.stroke();\n          }\n        }\n      } else {\n        const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize;\n        const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize;\n\n        const cols = Math.ceil(canvas.width / squareSize) + 3;\n        const rows = Math.ceil(canvas.height / squareSize) + 3;\n\n        for (let col = -2; col < cols; col++) {\n          for (let row = -2; row < rows; row++) {\n            const sx = col * squareSize + offsetX;\n            const sy = row * squareSize + offsetY;\n\n            const cellKey = `${col},${row}`;\n            const alpha = cellOpacities.current.get(cellKey);\n            if (alpha) {\n              ctx.globalAlpha = alpha;\n              ctx.fillStyle = hoverFillColor;\n              ctx.fillRect(sx, sy, squareSize, squareSize);\n              ctx.globalAlpha = 1;\n            }\n\n            ctx.strokeStyle = borderColor;\n            ctx.strokeRect(sx, sy, squareSize, squareSize);\n          }\n        }\n      }\n\n      const gradient = ctx.createRadialGradient(\n        canvas.width / 2,\n        canvas.height / 2,\n        0,\n        canvas.width / 2,\n        canvas.height / 2,\n        Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2\n      );\n      gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');\n      gradient.addColorStop(1, '#120F17');\n\n      ctx.fillStyle = gradient;\n      ctx.fillRect(0, 0, canvas.width, canvas.height);\n    };\n\n    const updateAnimation = () => {\n      const effectiveSpeed = Math.max(speed, 0.1);\n      const wrapX = isHex ? hexHoriz * 2 : squareSize;\n      const wrapY = isHex ? hexVert : isTri ? squareSize * 2 : squareSize;\n\n      switch (direction) {\n        case 'right':\n          gridOffset.current.x = (gridOffset.current.x - effectiveSpeed + wrapX) % wrapX;\n          break;\n        case 'left':\n          gridOffset.current.x = (gridOffset.current.x + effectiveSpeed + wrapX) % wrapX;\n          break;\n        case 'up':\n          gridOffset.current.y = (gridOffset.current.y + effectiveSpeed + wrapY) % wrapY;\n          break;\n        case 'down':\n          gridOffset.current.y = (gridOffset.current.y - effectiveSpeed + wrapY) % wrapY;\n          break;\n        case 'diagonal':\n          gridOffset.current.x = (gridOffset.current.x - effectiveSpeed + wrapX) % wrapX;\n          gridOffset.current.y = (gridOffset.current.y - effectiveSpeed + wrapY) % wrapY;\n          break;\n        default:\n          break;\n      }\n\n      updateCellOpacities();\n      drawGrid();\n      requestRef.current = requestAnimationFrame(updateAnimation);\n    };\n\n    const updateCellOpacities = () => {\n      const targets = new Map<string, number>();\n\n      if (hoveredSquareRef.current) {\n        targets.set(`${hoveredSquareRef.current.x},${hoveredSquareRef.current.y}`, 1);\n      }\n\n      if (hoverTrailAmount > 0) {\n        for (let i = 0; i < trailCells.current.length; i++) {\n          const t = trailCells.current[i];\n          const key = `${t.x},${t.y}`;\n          if (!targets.has(key)) {\n            targets.set(key, (trailCells.current.length - i) / (trailCells.current.length + 1));\n          }\n        }\n      }\n\n      for (const [key] of targets) {\n        if (!cellOpacities.current.has(key)) {\n          cellOpacities.current.set(key, 0);\n        }\n      }\n\n      for (const [key, opacity] of cellOpacities.current) {\n        const target = targets.get(key) || 0;\n        const next = opacity + (target - opacity) * 0.15;\n        if (next < 0.005) {\n          cellOpacities.current.delete(key);\n        } else {\n          cellOpacities.current.set(key, next);\n        }\n      }\n    };\n\n    const handleMouseMove = (event: MouseEvent) => {\n      const rect = canvas.getBoundingClientRect();\n      const mouseX = event.clientX - rect.left;\n      const mouseY = event.clientY - rect.top;\n\n      if (isHex) {\n        const colShift = Math.floor(gridOffset.current.x / hexHoriz);\n        const offsetX = ((gridOffset.current.x % hexHoriz) + hexHoriz) % hexHoriz;\n        const offsetY = ((gridOffset.current.y % hexVert) + hexVert) % hexVert;\n        const adjustedX = mouseX - offsetX;\n        const adjustedY = mouseY - offsetY;\n\n        const col = Math.round(adjustedX / hexHoriz);\n        const rowOffset = (col + colShift) % 2 !== 0 ? hexVert / 2 : 0;\n        const row = Math.round((adjustedY - rowOffset) / hexVert);\n\n        if (\n          !hoveredSquareRef.current ||\n          hoveredSquareRef.current.x !== col ||\n          hoveredSquareRef.current.y !== row\n        ) {\n          if (hoveredSquareRef.current && hoverTrailAmount > 0) {\n            trailCells.current.unshift({ ...hoveredSquareRef.current });\n            if (trailCells.current.length > hoverTrailAmount) trailCells.current.length = hoverTrailAmount;\n          }\n          hoveredSquareRef.current = { x: col, y: row };\n        }\n      } else if (isTri) {\n        const halfW = squareSize / 2;\n        const offsetX = ((gridOffset.current.x % halfW) + halfW) % halfW;\n        const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize;\n\n        const adjustedX = mouseX - offsetX;\n        const adjustedY = mouseY - offsetY;\n\n        const col = Math.round(adjustedX / halfW);\n        const row = Math.floor(adjustedY / squareSize);\n\n        if (\n          !hoveredSquareRef.current ||\n          hoveredSquareRef.current.x !== col ||\n          hoveredSquareRef.current.y !== row\n        ) {\n          if (hoveredSquareRef.current && hoverTrailAmount > 0) {\n            trailCells.current.unshift({ ...hoveredSquareRef.current });\n            if (trailCells.current.length > hoverTrailAmount) trailCells.current.length = hoverTrailAmount;\n          }\n          hoveredSquareRef.current = { x: col, y: row };\n        }\n      } else if (shape === 'circle') {\n        const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize;\n        const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize;\n\n        const adjustedX = mouseX - offsetX;\n        const adjustedY = mouseY - offsetY;\n\n        const col = Math.round(adjustedX / squareSize);\n        const row = Math.round(adjustedY / squareSize);\n\n        if (\n          !hoveredSquareRef.current ||\n          hoveredSquareRef.current.x !== col ||\n          hoveredSquareRef.current.y !== row\n        ) {\n          if (hoveredSquareRef.current && hoverTrailAmount > 0) {\n            trailCells.current.unshift({ ...hoveredSquareRef.current });\n            if (trailCells.current.length > hoverTrailAmount) trailCells.current.length = hoverTrailAmount;\n          }\n          hoveredSquareRef.current = { x: col, y: row };\n        }\n      } else {\n        const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize;\n        const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize;\n\n        const adjustedX = mouseX - offsetX;\n        const adjustedY = mouseY - offsetY;\n\n        const col = Math.floor(adjustedX / squareSize);\n        const row = Math.floor(adjustedY / squareSize);\n\n        if (\n          !hoveredSquareRef.current ||\n          hoveredSquareRef.current.x !== col ||\n          hoveredSquareRef.current.y !== row\n        ) {\n          if (hoveredSquareRef.current && hoverTrailAmount > 0) {\n            trailCells.current.unshift({ ...hoveredSquareRef.current });\n            if (trailCells.current.length > hoverTrailAmount) trailCells.current.length = hoverTrailAmount;\n          }\n          hoveredSquareRef.current = { x: col, y: row };\n        }\n      }\n    };\n\n    const handleMouseLeave = () => {\n      if (hoveredSquareRef.current && hoverTrailAmount > 0) {\n        trailCells.current.unshift({ ...hoveredSquareRef.current });\n        if (trailCells.current.length > hoverTrailAmount) trailCells.current.length = hoverTrailAmount;\n      }\n      hoveredSquareRef.current = null;\n    };\n\n    canvas.addEventListener('mousemove', handleMouseMove);\n    canvas.addEventListener('mouseleave', handleMouseLeave);\n    let isVisible = false;\n    let isPageVisible = !document.hidden;\n\n    const tryStart = () => {\n      if (isVisible && isPageVisible && !requestRef.current) {\n        requestRef.current = requestAnimationFrame(updateAnimation);\n      }\n    };\n    const tryStop = () => {\n      if (requestRef.current) {\n        cancelAnimationFrame(requestRef.current);\n        requestRef.current = null;\n      }\n    };\n\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        isVisible = entry.isIntersecting;\n        isVisible ? tryStart() : tryStop();\n      },\n      { threshold: 0 }\n    );\n    io.observe(canvas);\n\n    const onVisibility = () => {\n      isPageVisible = !document.hidden;\n      isPageVisible ? tryStart() : tryStop();\n    };\n    document.addEventListener('visibilitychange', onVisibility);\n\n    tryStart();\n\n    return () => {\n      window.removeEventListener('resize', resizeCanvas);\n      tryStop();\n      io.disconnect();\n      document.removeEventListener('visibilitychange', onVisibility);\n      canvas.removeEventListener('mousemove', handleMouseMove);\n      canvas.removeEventListener('mouseleave', handleMouseLeave);\n    };\n  }, [direction, speed, borderColor, hoverFillColor, squareSize, shape, hoverTrailAmount]);\n\n  return <canvas ref={canvasRef} className=\"shapegrid-canvas\"></canvas>;\n};\n\nexport default ShapeGrid;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}