{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "use-pixel-canvas", "title": "Use Pixel Canvas", "author": "designbycode", "dependencies": [ "react" ], "registryDependencies": [ "https://ui.designbycode.co.za/r/pixel-canvas.json" ], "files": [ { "path": "resources/js/registry/new-york/hooks/use-pixel-canvas.ts", "content": "'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type {\n AnimationDirection,\n PixelConfig,\n PixelState,\n} from '@/registry/new-york/lib/pixel-canvas';\nimport {\n calculateDelay,\n createPixelState,\n defaultPixelConfig,\n drawPixel,\n updatePixelAppear,\n updatePixelDisappear,\n} from '@/registry/new-york/lib/pixel-canvas';\n\ninterface UsePixelCanvasOptions extends Partial {\n /**\n * Controls whether animation runs continuously\n * - When true: animation runs automatically and continuously\n * - When false: animation only runs when triggered via JS or mouse\n */\n active?: boolean;\n /**\n * Enable mouse interaction (hover triggers animation)\n * - When true: mouseenter triggers appear, mouseleave triggers disappear\n * - When false: mouse events are ignored\n */\n mouseActive?: boolean;\n /** @deprecated Use `active` instead. Auto-start animation on mount */\n autoStart?: boolean;\n /** @deprecated Use `mouseActive` instead. Trigger animation on hover */\n hoverTrigger?: boolean;\n}\n\ninterface UsePixelCanvasReturn {\n canvasRef: React.RefObject;\n containerRef: React.RefObject;\n isAnimating: boolean;\n triggerAppear: () => void;\n triggerDisappear: () => void;\n reset: () => void;\n}\n\nexport function usePixelCanvas(\n options: UsePixelCanvasOptions = {},\n): UsePixelCanvasReturn {\n const config: PixelConfig = { ...defaultPixelConfig, ...options };\n\n // Handle both old and new prop names for backwards compatibility\n const {\n active,\n mouseActive,\n autoStart = false,\n hoverTrigger = true,\n } = options;\n\n // New props take precedence over deprecated ones\n const shouldAutoStart = active ?? autoStart;\n const shouldReactToMouse = mouseActive ?? hoverTrigger;\n\n const canvasRef = useRef(null);\n const containerRef = useRef(null);\n const pixelsRef = useRef([]);\n const animationRef = useRef(null);\n const directionRef = useRef('appear');\n const isInitializedRef = useRef(false);\n const activeRef = useRef(shouldAutoStart);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const speedMultiplier = config.speed * 0.001;\n\n // Keep activeRef in sync with prop\n useEffect(() => {\n activeRef.current = shouldAutoStart;\n }, [shouldAutoStart]);\n\n const initPixels = useCallback(() => {\n const canvas = canvasRef.current;\n\n if (!canvas) {\n return;\n }\n\n const ctx = canvas.getContext('2d');\n\n if (!ctx) {\n return;\n }\n\n const rect = canvas.getBoundingClientRect();\n const width = Math.floor(rect.width);\n const height = Math.floor(rect.height);\n\n if (width <= 0 || height <= 0) {\n return;\n }\n\n // Set canvas size with device pixel ratio for crisp rendering\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n ctx.scale(dpr, dpr);\n\n const pixels: PixelState[] = [];\n const reducedMotion = window.matchMedia(\n '(prefers-reduced-tabs: reduce)',\n ).matches;\n\n for (let x = 0; x < width; x += config.gap) {\n for (let y = 0; y < height; y += config.gap) {\n const color =\n config.colors[\n Math.floor(Math.random() * config.colors.length)\n ];\n const delay = reducedMotion\n ? 0\n : calculateDelay(x, y, width, height, config.animationType);\n\n pixels.push(\n createPixelState(\n x,\n y,\n color,\n delay,\n speedMultiplier,\n config.minSize,\n config.maxSize,\n width,\n height,\n ),\n );\n }\n }\n\n pixelsRef.current = pixels;\n isInitializedRef.current = true;\n }, [\n config.gap,\n config.colors,\n config.animationType,\n config.minSize,\n config.maxSize,\n speedMultiplier,\n ]);\n\n const animate = useCallback(() => {\n const canvas = canvasRef.current;\n\n if (!canvas) {\n return;\n }\n\n const ctx = canvas.getContext('2d');\n\n if (!ctx) {\n return;\n }\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n const width = canvas.width / dpr;\n const height = canvas.height / dpr;\n\n ctx.clearRect(0, 0, width, height);\n\n let allIdle = true;\n const direction = directionRef.current;\n\n pixelsRef.current = pixelsRef.current.map((pixel) => {\n let updated: PixelState;\n\n if (direction === 'appear') {\n updated = updatePixelAppear(pixel, config.shimmerIntensity);\n } else {\n updated = updatePixelDisappear(pixel);\n }\n\n // Draw pixel if it has size > 0 (clamp to prevent negative values)\n const safeSize = Math.max(0, updated.size);\n\n if (safeSize > 0.01) {\n allIdle = false;\n drawPixel(\n ctx,\n updated.x,\n updated.y,\n safeSize,\n config.maxSize,\n updated.color,\n config.shape,\n );\n } else if (!updated.isIdle) {\n allIdle = false;\n }\n\n return updated;\n });\n\n // For disappear: stop when all pixels are gone\n // For appear: never stop - keep shimmering\n if (direction === 'disappear' && allIdle) {\n setIsAnimating(false);\n\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current);\n animationRef.current = null;\n }\n\n // Reset pixels for next appear animation\n initPixels();\n\n return;\n }\n\n animationRef.current = requestAnimationFrame(animate);\n }, [config.shimmerIntensity, config.maxSize, config.shape, initPixels]);\n\n const startAnimation = useCallback(\n (direction: AnimationDirection) => {\n // If disappearing, just change direction - don't reinit\n if (direction === 'disappear') {\n directionRef.current = direction;\n\n if (!animationRef.current) {\n setIsAnimating(true);\n animationRef.current = requestAnimationFrame(animate);\n }\n\n return;\n }\n\n // For appear, always reset pixel states for fresh animation\n initPixels();\n\n directionRef.current = direction;\n setIsAnimating(true);\n\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current);\n }\n\n animationRef.current = requestAnimationFrame(animate);\n },\n [animate, initPixels],\n );\n\n const triggerAppear = useCallback(() => {\n startAnimation('appear');\n }, [startAnimation]);\n\n const triggerDisappear = useCallback(() => {\n startAnimation('disappear');\n }, [startAnimation]);\n\n const reset = useCallback(() => {\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current);\n animationRef.current = null;\n }\n\n setIsAnimating(false);\n directionRef.current = 'appear';\n initPixels();\n\n const canvas = canvasRef.current;\n\n if (canvas) {\n const ctx = canvas.getContext('2d');\n\n if (ctx) {\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);\n }\n }\n }, [initPixels]);\n\n // Initialize and handle resize\n useEffect(() => {\n initPixels();\n\n const container = containerRef.current;\n\n if (!container) {\n return;\n }\n\n const resizeObserver = new ResizeObserver(() => {\n initPixels();\n\n // Restart animation if it was running and we're in active mode\n if (activeRef.current && animationRef.current) {\n triggerAppear();\n }\n });\n\n resizeObserver.observe(container);\n\n return () => {\n resizeObserver.disconnect();\n\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current);\n }\n };\n }, [initPixels, triggerAppear]);\n\n // Handle mouse events\n useEffect(() => {\n if (!shouldReactToMouse) {\n return;\n }\n\n const container = containerRef.current;\n\n if (!container) {\n return;\n }\n\n const handleMouseEnter = () => {\n // Reset and start fresh appear animation\n triggerAppear();\n };\n\n const handleMouseLeave = () => {\n triggerDisappear();\n };\n\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n if (!config.noFocus) {\n container.addEventListener('focusin', handleMouseEnter);\n container.addEventListener('focusout', handleMouseLeave);\n }\n\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n container.removeEventListener('focusin', handleMouseEnter);\n container.removeEventListener('focusout', handleMouseLeave);\n };\n }, [shouldReactToMouse, config.noFocus, triggerAppear, triggerDisappear]);\n\n // Handle active prop - continuous animation\n useEffect(() => {\n if (shouldAutoStart) {\n triggerAppear();\n } else if (!shouldReactToMouse) {\n // If neither active nor mouseActive, clear canvas\n reset();\n }\n }, [shouldAutoStart, shouldReactToMouse, triggerAppear, reset]);\n\n return {\n canvasRef,\n containerRef,\n isAnimating,\n triggerAppear,\n triggerDisappear,\n reset,\n };\n}\n", "type": "registry:hook" } ], "meta": { "category": "hooks", "version": "1.0.0" }, "type": "registry:hook" }