{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pixel-highlight",
  "type": "registry:ui",
  "title": "Pixel Highlight",
  "description": "Animated pixel-based text reveal with directional shimmer using canvas-rendered particles.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["motion", "clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/pixel-highlight.tsx",
      "content": "'use client';\nimport React, { useRef, useEffect, useState } from 'react';\nimport { useInView } from 'motion/react';\nimport { cn } from '@/lib/utils';\n\nclass Pixel {\n  width: number;\n  height: number;\n  ctx: CanvasRenderingContext2D;\n  x: number;\n  y: number;\n  color: string;\n  speed: number;\n  size: number;\n  sizeStep: number;\n  minSize: number;\n  maxSizeInteger: number;\n  maxSize: number;\n  delay: number;\n  counter: number;\n  counterStep: number;\n  isIdle: boolean;\n  isReverse: boolean;\n  isShimmer: boolean;\n\n  constructor(\n    canvas: HTMLCanvasElement,\n    context: CanvasRenderingContext2D,\n    x: number,\n    y: number,\n    color: string,\n    speed: number,\n    delay: number,\n  ) {\n    this.width = canvas.width;\n    this.height = canvas.height;\n    this.ctx = context;\n    this.x = x;\n    this.y = y;\n    this.color = color;\n    this.speed = this.getRandomValue(0.1, 0.9) * speed;\n    this.size = 0;\n    this.sizeStep = Math.random() * 0.4;\n    this.minSize = 0.5;\n    this.maxSizeInteger = 2;\n    this.maxSize = this.getRandomValue(this.minSize, this.maxSizeInteger);\n    this.delay = delay;\n    this.counter = 0;\n    this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;\n    this.isIdle = false;\n    this.isReverse = false;\n    this.isShimmer = false;\n  }\n\n  getRandomValue(min: number, max: number) {\n    return Math.random() * (max - min) + min;\n  }\n\n  draw() {\n    const centerOffset = this.maxSizeInteger * 0.5 - this.size * 0.5;\n    this.ctx.fillStyle = this.color;\n    this.ctx.fillRect(\n      this.x + centerOffset,\n      this.y + centerOffset,\n      this.size,\n      this.size,\n    );\n  }\n\n  appear() {\n    this.isIdle = false;\n    if (this.counter <= this.delay) {\n      this.counter += this.counterStep;\n      return;\n    }\n    if (this.size >= this.maxSize) {\n      this.isShimmer = true;\n    }\n    if (this.isShimmer) {\n      this.shimmer();\n    } else {\n      this.size += this.sizeStep;\n    }\n    this.draw();\n  }\n\n  shimmer() {\n    if (this.size >= this.maxSize) {\n      this.isReverse = true;\n    } else if (this.size <= this.minSize) {\n      this.isReverse = false;\n    }\n    if (this.isReverse) {\n      this.size -= this.speed;\n    } else {\n      this.size += this.speed;\n    }\n  }\n}\n\nfunction getEffectiveSpeed(value: number, reducedMotion: boolean) {\n  const min = 0;\n  const max = 100;\n  const throttle = 0.001;\n  if (value <= min || reducedMotion) {\n    return min;\n  } else if (value >= max) {\n    return max * throttle;\n  } else {\n    return value * throttle;\n  }\n}\n\ninterface PixelHighlightProps {\n  text?: string;\n  children?: React.ReactNode;\n  className?: string;\n  gap?: number;\n  speed?: number;\n  colors?: string;\n  opacity?: number;\n  direction?:\n    | 'center'\n    | 'top'\n    | 'bottom'\n    | 'left'\n    | 'right'\n    | 'top-left'\n    | 'top-right'\n    | 'bottom-left'\n    | 'bottom-right';\n  fontSize?: string | number;\n  fontWeight?: string | number;\n  fontFamily?: string;\n}\n\nexport function PixelHighlight({\n  text,\n  children,\n  className = '',\n  gap = 3,\n  speed = 80,\n  colors = '#fecdd3,#fda4af,#e11d48',\n  opacity = 1,\n  direction = 'center',\n  fontSize = 20,\n  fontWeight = 'bold',\n  fontFamily = 'sans-serif',\n}: PixelHighlightProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const pixelsRef = useRef<Pixel[]>([]);\n  const animationRef = useRef<number | null>(null);\n  const timePreviousRef = useRef<number>(0);\n  const isInView = useInView(containerRef, { amount: 0.3, once: false });\n  const [svgMask, setSvgMask] = useState('');\n  const hasAnimatedRef = useRef(false);\n\n  const content = text || React.Children.toArray(children).join('');\n\n  useEffect(() => {\n    const updateSvgMask = () => {\n      const responsiveFontSize =\n        typeof fontSize === 'number' ? `${fontSize}vw` : fontSize;\n      const newSvgMask = `<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><text x='50%' y='50%' font-size='${responsiveFontSize}' font-weight='${fontWeight}' text-anchor='middle' dominant-baseline='middle' font-family='${fontFamily}'>${content}</text></svg>`;\n      setSvgMask(newSvgMask);\n    };\n\n    updateSvgMask();\n    window.addEventListener('resize', updateSvgMask);\n    return () => window.removeEventListener('resize', updateSvgMask);\n  }, [content, fontSize, fontWeight, fontFamily]);\n\n  useEffect(() => {\n    if (!isInView) {\n      hasAnimatedRef.current = false;\n      if (animationRef.current !== null) {\n        cancelAnimationFrame(animationRef.current);\n        animationRef.current = null;\n      }\n      const ctx = canvasRef.current?.getContext('2d');\n      if (ctx && canvasRef.current) {\n        ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);\n      }\n      pixelsRef.current = [];\n    }\n  }, [isInView]);\n\n  useEffect(() => {\n    if (!isInView || hasAnimatedRef.current) return;\n\n    const reducedMotionValue = window.matchMedia(\n      '(prefers-reduced-motion: reduce)',\n    ).matches;\n\n    const getOriginPoint = (width: number, height: number) => {\n      switch (direction) {\n        case 'top':\n          return { x: width / 2, y: 0 };\n        case 'bottom':\n          return { x: width / 2, y: height };\n        case 'left':\n          return { x: 0, y: height / 2 };\n        case 'right':\n          return { x: width, y: height / 2 };\n        case 'top-left':\n          return { x: 0, y: 0 };\n        case 'top-right':\n          return { x: width, y: 0 };\n        case 'bottom-left':\n          return { x: 0, y: height };\n        case 'bottom-right':\n          return { x: width, y: height };\n        case 'center':\n        default:\n          return { x: width / 2, y: height / 2 };\n      }\n    };\n\n    const initPixels = () => {\n      if (!containerRef.current || !canvasRef.current) return;\n\n      const rect = containerRef.current.getBoundingClientRect();\n      const width = Math.floor(rect.width);\n      const height = Math.floor(rect.height);\n      const ctx = canvasRef.current.getContext('2d');\n\n      if (!ctx) return;\n\n      canvasRef.current.width = width;\n      canvasRef.current.height = height;\n\n      const origin = getOriginPoint(width, height);\n      const colorsArray = colors.split(',');\n      const pxs: Pixel[] = [];\n\n      for (let x = 0; x < width; x += parseInt(gap.toString(), 10)) {\n        for (let y = 0; y < height; y += parseInt(gap.toString(), 10)) {\n          const color =\n            colorsArray[Math.floor(Math.random() * colorsArray.length)];\n          const dx = x - origin.x;\n          const dy = y - origin.y;\n          const distance = Math.sqrt(dx * dx + dy * dy);\n          const delay = reducedMotionValue ? 0 : distance;\n\n          pxs.push(\n            new Pixel(\n              canvasRef.current!,\n              ctx,\n              x,\n              y,\n              color,\n              getEffectiveSpeed(speed, reducedMotionValue),\n              delay,\n            ),\n          );\n        }\n      }\n\n      pixelsRef.current = pxs;\n    };\n\n    const doAnimate = () => {\n      animationRef.current = requestAnimationFrame(doAnimate);\n\n      const timeNow = performance.now();\n      if (timePreviousRef.current === 0) timePreviousRef.current = timeNow;\n      const timePassed = timeNow - timePreviousRef.current;\n      const timeInterval = 1000 / 60;\n\n      if (timePassed < timeInterval) return;\n      timePreviousRef.current = timeNow - (timePassed % timeInterval);\n\n      const ctx = canvasRef.current?.getContext('2d');\n      if (!ctx || !canvasRef.current) return;\n\n      ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);\n\n      let allIdle = true;\n      for (let i = 0; i < pixelsRef.current.length; i++) {\n        const pixel = pixelsRef.current[i];\n        pixel.appear();\n        if (!pixel.isIdle) {\n          allIdle = false;\n        }\n      }\n\n      if (allIdle && animationRef.current !== null) {\n        cancelAnimationFrame(animationRef.current);\n        hasAnimatedRef.current = true;\n      }\n    };\n\n    initPixels();\n    animationRef.current = requestAnimationFrame(doAnimate);\n\n    return () => {\n      if (animationRef.current !== null) {\n        cancelAnimationFrame(animationRef.current);\n      }\n    };\n  }, [gap, speed, colors, direction, isInView]);\n\n  const dataUrlMask = `url(\"data:image/svg+xml,${encodeURIComponent(\n    svgMask,\n  )}\")`;\n\n  return (\n    <div ref={containerRef} className={cn('relative inline-block', className)}>\n      <div\n        className='absolute inset-0 flex items-center justify-center'\n        style={{\n          maskImage: dataUrlMask,\n          WebkitMaskImage: dataUrlMask,\n          maskSize: 'contain',\n          WebkitMaskSize: 'contain',\n          maskRepeat: 'no-repeat',\n          WebkitMaskRepeat: 'no-repeat',\n          maskPosition: 'center',\n          WebkitMaskPosition: 'center',\n        }}\n      >\n        <canvas ref={canvasRef} className='h-full w-full' style={{ opacity }} />\n      </div>\n      <span className='sr-only'>{content}</span>\n    </div>\n  );\n}\n\nexport default PixelHighlight;\n"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
