{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FuzzyText-TS-CSS",
	"title": "FuzzyText",
	"description": "Vibrating fuzzy text with controllable hover intensity.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "FuzzyText/FuzzyText.tsx",
			"content": "import React, { useEffect, useRef } from 'react';\n\ninterface FuzzyTextProps {\n  children: React.ReactNode;\n  fontSize?: number | string;\n  fontWeight?: string | number;\n  fontFamily?: string;\n  color?: string;\n  enableHover?: boolean;\n  baseIntensity?: number;\n  hoverIntensity?: number;\n  fuzzRange?: number;\n  fps?: number;\n  direction?: 'horizontal' | 'vertical' | 'both';\n  transitionDuration?: number;\n  clickEffect?: boolean;\n  glitchMode?: boolean;\n  glitchInterval?: number;\n  glitchDuration?: number;\n  gradient?: string[] | null;\n  letterSpacing?: number;\n  className?: string;\n}\n\nconst FuzzyText: React.FC<FuzzyTextProps> = ({\n  children,\n  fontSize = 'clamp(2rem, 8vw, 8rem)',\n  fontWeight = 900,\n  fontFamily = 'inherit',\n  color = '#fff',\n  enableHover = true,\n  baseIntensity = 0.18,\n  hoverIntensity = 0.5,\n  fuzzRange = 30,\n  fps = 60,\n  direction = 'horizontal',\n  transitionDuration = 0,\n  clickEffect = false,\n  glitchMode = false,\n  glitchInterval = 2000,\n  glitchDuration = 200,\n  gradient = null,\n  letterSpacing = 0,\n  className = ''\n}) => {\n  const canvasRef = useRef<HTMLCanvasElement & { cleanupFuzzyText?: () => void }>(null);\n\n  useEffect(() => {\n    let animationFrameId: number;\n    let isCancelled = false;\n    let glitchTimeoutId: ReturnType<typeof setTimeout>;\n    let glitchEndTimeoutId: ReturnType<typeof setTimeout>;\n    let clickTimeoutId: ReturnType<typeof setTimeout>;\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const init = async () => {\n      const ctx = canvas.getContext('2d');\n      if (!ctx) return;\n\n      const computedFontFamily =\n        fontFamily === 'inherit' ? window.getComputedStyle(canvas).fontFamily || 'sans-serif' : fontFamily;\n\n      const fontSizeStr = typeof fontSize === 'number' ? `${fontSize}px` : fontSize;\n      const fontString = `${fontWeight} ${fontSizeStr} ${computedFontFamily}`;\n\n      try {\n        await document.fonts.load(fontString);\n      } catch {\n        await document.fonts.ready;\n      }\n      if (isCancelled) return;\n\n      let numericFontSize: number;\n      if (typeof fontSize === 'number') {\n        numericFontSize = fontSize;\n      } else {\n        const temp = document.createElement('span');\n        temp.style.fontSize = fontSize;\n        document.body.appendChild(temp);\n        const computedSize = window.getComputedStyle(temp).fontSize;\n        numericFontSize = parseFloat(computedSize);\n        document.body.removeChild(temp);\n      }\n\n      const text = React.Children.toArray(children).join('');\n\n      const offscreen = document.createElement('canvas');\n      const offCtx = offscreen.getContext('2d');\n      if (!offCtx) return;\n\n      offCtx.font = `${fontWeight} ${fontSizeStr} ${computedFontFamily}`;\n      offCtx.textBaseline = 'alphabetic';\n\n      let totalWidth = 0;\n      if (letterSpacing !== 0) {\n        for (const char of text) {\n          totalWidth += offCtx.measureText(char).width + letterSpacing;\n        }\n        totalWidth -= letterSpacing;\n      } else {\n        totalWidth = offCtx.measureText(text).width;\n      }\n\n      const metrics = offCtx.measureText(text);\n      const actualLeft = metrics.actualBoundingBoxLeft ?? 0;\n      const actualRight = letterSpacing !== 0 ? totalWidth : (metrics.actualBoundingBoxRight ?? metrics.width);\n      const actualAscent = metrics.actualBoundingBoxAscent ?? numericFontSize;\n      const actualDescent = metrics.actualBoundingBoxDescent ?? numericFontSize * 0.2;\n\n      const textBoundingWidth = Math.ceil(letterSpacing !== 0 ? totalWidth : actualLeft + actualRight);\n      const tightHeight = Math.ceil(actualAscent + actualDescent);\n\n      const extraWidthBuffer = 10;\n      const offscreenWidth = textBoundingWidth + extraWidthBuffer;\n\n      offscreen.width = offscreenWidth;\n      offscreen.height = tightHeight;\n\n      const xOffset = extraWidthBuffer / 2;\n      offCtx.font = `${fontWeight} ${fontSizeStr} ${computedFontFamily}`;\n      offCtx.textBaseline = 'alphabetic';\n\n      if (gradient && Array.isArray(gradient) && gradient.length >= 2) {\n        const grad = offCtx.createLinearGradient(0, 0, offscreenWidth, 0);\n        gradient.forEach((c, i) => grad.addColorStop(i / (gradient.length - 1), c));\n        offCtx.fillStyle = grad;\n      } else {\n        offCtx.fillStyle = color;\n      }\n\n      if (letterSpacing !== 0) {\n        let xPos = xOffset;\n        for (const char of text) {\n          offCtx.fillText(char, xPos, actualAscent);\n          xPos += offCtx.measureText(char).width + letterSpacing;\n        }\n      } else {\n        offCtx.fillText(text, xOffset - actualLeft, actualAscent);\n      }\n\n      const horizontalMargin = fuzzRange + 20;\n      const verticalMargin = direction === 'vertical' || direction === 'both' ? fuzzRange + 10 : 0;\n      canvas.width = offscreenWidth + horizontalMargin * 2;\n      canvas.height = tightHeight + verticalMargin * 2;\n      ctx.translate(horizontalMargin, verticalMargin);\n\n      const interactiveLeft = horizontalMargin + xOffset;\n      const interactiveTop = verticalMargin;\n      const interactiveRight = interactiveLeft + textBoundingWidth;\n      const interactiveBottom = interactiveTop + tightHeight;\n\n      let isHovering = false;\n      let isClicking = false;\n      let isGlitching = false;\n      let currentIntensity = baseIntensity;\n      let targetIntensity = baseIntensity;\n      let lastFrameTime = 0;\n      const frameDuration = 1000 / fps;\n\n      const startGlitchLoop = () => {\n        if (!glitchMode || isCancelled) return;\n        glitchTimeoutId = setTimeout(() => {\n          if (isCancelled) return;\n          isGlitching = true;\n          glitchEndTimeoutId = setTimeout(() => {\n            isGlitching = false;\n            startGlitchLoop();\n          }, glitchDuration);\n        }, glitchInterval);\n      };\n\n      if (glitchMode) startGlitchLoop();\n\n      const run = (timestamp: number) => {\n        if (isCancelled) return;\n\n        if (timestamp - lastFrameTime < frameDuration) {\n          animationFrameId = window.requestAnimationFrame(run);\n          return;\n        }\n        lastFrameTime = timestamp;\n\n        ctx.clearRect(\n          -fuzzRange - 20,\n          -fuzzRange - 10,\n          offscreenWidth + 2 * (fuzzRange + 20),\n          tightHeight + 2 * (fuzzRange + 10)\n        );\n\n        if (isClicking) {\n          targetIntensity = 1;\n        } else if (isGlitching) {\n          targetIntensity = 1;\n        } else if (isHovering) {\n          targetIntensity = hoverIntensity;\n        } else {\n          targetIntensity = baseIntensity;\n        }\n\n        if (transitionDuration > 0) {\n          const step = 1 / (transitionDuration / frameDuration);\n          if (currentIntensity < targetIntensity) {\n            currentIntensity = Math.min(currentIntensity + step, targetIntensity);\n          } else if (currentIntensity > targetIntensity) {\n            currentIntensity = Math.max(currentIntensity - step, targetIntensity);\n          }\n        } else {\n          currentIntensity = targetIntensity;\n        }\n\n        for (let j = 0; j < tightHeight; j++) {\n          let dx = 0,\n            dy = 0;\n          if (direction === 'horizontal' || direction === 'both') {\n            dx = Math.floor(currentIntensity * (Math.random() - 0.5) * fuzzRange);\n          }\n          if (direction === 'vertical' || direction === 'both') {\n            dy = Math.floor(currentIntensity * (Math.random() - 0.5) * fuzzRange * 0.5);\n          }\n          ctx.drawImage(offscreen, 0, j, offscreenWidth, 1, dx, j + dy, offscreenWidth, 1);\n        }\n        animationFrameId = window.requestAnimationFrame(run);\n      };\n\n      animationFrameId = window.requestAnimationFrame(run);\n\n      const isInsideTextArea = (x: number, y: number) =>\n        x >= interactiveLeft && x <= interactiveRight && y >= interactiveTop && y <= interactiveBottom;\n\n      const handleMouseMove = (e: MouseEvent) => {\n        if (!enableHover) return;\n        const rect = canvas.getBoundingClientRect();\n        const x = e.clientX - rect.left;\n        const y = e.clientY - rect.top;\n        isHovering = isInsideTextArea(x, y);\n      };\n\n      const handleMouseLeave = () => {\n        isHovering = false;\n      };\n\n      const handleClick = () => {\n        if (!clickEffect) return;\n        isClicking = true;\n        clearTimeout(clickTimeoutId);\n        clickTimeoutId = setTimeout(() => {\n          isClicking = false;\n        }, 150);\n      };\n\n      const handleTouchMove = (e: TouchEvent) => {\n        if (!enableHover) return;\n        e.preventDefault();\n        const rect = canvas.getBoundingClientRect();\n        const touch = e.touches[0];\n        const x = touch.clientX - rect.left;\n        const y = touch.clientY - rect.top;\n        isHovering = isInsideTextArea(x, y);\n      };\n\n      const handleTouchEnd = () => {\n        isHovering = false;\n      };\n\n      if (enableHover) {\n        canvas.addEventListener('mousemove', handleMouseMove);\n        canvas.addEventListener('mouseleave', handleMouseLeave);\n        canvas.addEventListener('touchmove', handleTouchMove, { passive: false });\n        canvas.addEventListener('touchend', handleTouchEnd);\n      }\n\n      if (clickEffect) {\n        canvas.addEventListener('click', handleClick);\n      }\n\n      const cleanup = () => {\n        window.cancelAnimationFrame(animationFrameId);\n        clearTimeout(glitchTimeoutId);\n        clearTimeout(glitchEndTimeoutId);\n        clearTimeout(clickTimeoutId);\n        if (enableHover) {\n          canvas.removeEventListener('mousemove', handleMouseMove);\n          canvas.removeEventListener('mouseleave', handleMouseLeave);\n          canvas.removeEventListener('touchmove', handleTouchMove);\n          canvas.removeEventListener('touchend', handleTouchEnd);\n        }\n        if (clickEffect) {\n          canvas.removeEventListener('click', handleClick);\n        }\n      };\n\n      canvas.cleanupFuzzyText = cleanup;\n    };\n\n    init();\n\n    return () => {\n      isCancelled = true;\n      window.cancelAnimationFrame(animationFrameId);\n      clearTimeout(glitchTimeoutId);\n      clearTimeout(glitchEndTimeoutId);\n      clearTimeout(clickTimeoutId);\n      if (canvas && canvas.cleanupFuzzyText) {\n        canvas.cleanupFuzzyText();\n      }\n    };\n  }, [\n    children,\n    fontSize,\n    fontWeight,\n    fontFamily,\n    color,\n    enableHover,\n    baseIntensity,\n    hoverIntensity,\n    fuzzRange,\n    fps,\n    direction,\n    transitionDuration,\n    clickEffect,\n    glitchMode,\n    glitchInterval,\n    glitchDuration,\n    gradient,\n    letterSpacing\n  ]);\n\n  return <canvas ref={canvasRef} className={className} />;\n};\n\nexport default FuzzyText;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}