{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "TextPressure-JS-CSS",
	"title": "TextPressure",
	"description": "Characters scale / warp interactively based on pointer pressure zone.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "TextPressure/TextPressure.jsx",
			"content": "// Component ported from https://codepen.io/JuanFuentes/full/rgXKGQ\n\nimport { useEffect, useRef, useState, useMemo, useCallback } from 'react';\n\nconst dist = (a, b) => {\n  const dx = b.x - a.x;\n  const dy = b.y - a.y;\n  return Math.sqrt(dx * dx + dy * dy);\n};\n\nconst getAttr = (distance, maxDist, minVal, maxVal) => {\n  const val = maxVal - Math.abs((maxVal * distance) / maxDist);\n  return Math.max(minVal, val + minVal);\n};\n\nconst debounce = (func, delay) => {\n  let timeoutId;\n  return (...args) => {\n    clearTimeout(timeoutId);\n    timeoutId = setTimeout(() => {\n      func.apply(this, args);\n    }, delay);\n  };\n};\n\nconst TextPressure = ({\n  text = 'Compressa',\n  fontFamily = 'Roboto Flex',\n  fontUrl = 'https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wdth,wght@8..144,25..151,100..1000&display=swap',\n\n  width = true,\n  weight = true,\n  italic = true,\n  alpha = false,\n\n  flex = true,\n  stroke = false,\n  scale = false,\n\n  textColor = '#FFFFFF',\n  strokeColor = '#FF0000',\n  className = '',\n\n  minFontSize = 24\n}) => {\n  const containerRef = useRef(null);\n  const titleRef = useRef(null);\n  const spansRef = useRef([]);\n\n  const mouseRef = useRef({ x: 0, y: 0 });\n  const cursorRef = useRef({ x: 0, y: 0 });\n\n  const [fontSize, setFontSize] = useState(minFontSize);\n  const [scaleY, setScaleY] = useState(1);\n  const [lineHeight, setLineHeight] = useState(1);\n\n  const chars = text.split('');\n\n  useEffect(() => {\n    const handleMouseMove = e => {\n      cursorRef.current.x = e.clientX;\n      cursorRef.current.y = e.clientY;\n    };\n    const handleTouchMove = e => {\n      const t = e.touches[0];\n      cursorRef.current.x = t.clientX;\n      cursorRef.current.y = t.clientY;\n    };\n\n    window.addEventListener('mousemove', handleMouseMove);\n    window.addEventListener('touchmove', handleTouchMove, { passive: true });\n\n    if (containerRef.current) {\n      const { left, top, width, height } = containerRef.current.getBoundingClientRect();\n      mouseRef.current.x = left + width / 2;\n      mouseRef.current.y = top + height / 2;\n      cursorRef.current.x = mouseRef.current.x;\n      cursorRef.current.y = mouseRef.current.y;\n    }\n\n    return () => {\n      window.removeEventListener('mousemove', handleMouseMove);\n      window.removeEventListener('touchmove', handleTouchMove);\n    };\n  }, []);\n\n  const setSize = useCallback(() => {\n    if (!containerRef.current || !titleRef.current) return;\n\n    const { width: containerW, height: containerH } = containerRef.current.getBoundingClientRect();\n\n    let newFontSize = containerW / (chars.length / 2);\n    newFontSize = Math.max(newFontSize, minFontSize);\n\n    setFontSize(newFontSize);\n    setScaleY(1);\n    setLineHeight(1);\n\n    requestAnimationFrame(() => {\n      if (!titleRef.current) return;\n      const textRect = titleRef.current.getBoundingClientRect();\n\n      if (scale && textRect.height > 0) {\n        const yRatio = containerH / textRect.height;\n        setScaleY(yRatio);\n        setLineHeight(yRatio);\n      }\n    });\n  }, [chars.length, minFontSize, scale]);\n\n  useEffect(() => {\n    const debouncedSetSize = debounce(setSize, 100);\n    debouncedSetSize();\n    window.addEventListener('resize', debouncedSetSize);\n    return () => window.removeEventListener('resize', debouncedSetSize);\n  }, [setSize]);\n\n  useEffect(() => {\n    let rafId;\n    const animate = () => {\n      mouseRef.current.x += (cursorRef.current.x - mouseRef.current.x) / 15;\n      mouseRef.current.y += (cursorRef.current.y - mouseRef.current.y) / 15;\n\n      if (titleRef.current) {\n        const titleRect = titleRef.current.getBoundingClientRect();\n        const maxDist = titleRect.width / 2;\n\n        spansRef.current.forEach(span => {\n          if (!span) return;\n\n          const rect = span.getBoundingClientRect();\n          const charCenter = {\n            x: rect.x + rect.width / 2,\n            y: rect.y + rect.height / 2\n          };\n\n          const d = dist(mouseRef.current, charCenter);\n\n          const wdth = width ? Math.floor(getAttr(d, maxDist, 5, 200)) : 100;\n          const wght = weight ? Math.floor(getAttr(d, maxDist, 100, 900)) : 400;\n          const italVal = italic ? getAttr(d, maxDist, 0, 1).toFixed(2) : 0;\n          const alphaVal = alpha ? getAttr(d, maxDist, 0, 1).toFixed(2) : 1;\n\n          const newFontVariationSettings = `'wght' ${wght}, 'wdth' ${wdth}, 'ital' ${italVal}`;\n\n          if (span.style.fontVariationSettings !== newFontVariationSettings) {\n            span.style.fontVariationSettings = newFontVariationSettings;\n          }\n          if (alpha && span.style.opacity !== alphaVal) {\n            span.style.opacity = alphaVal;\n          }\n        });\n      }\n\n      rafId = requestAnimationFrame(animate);\n    };\n\n    animate();\n    return () => cancelAnimationFrame(rafId);\n  }, [width, weight, italic, alpha]);\n\n  const styleElement = useMemo(() => {\n    return (\n      <style>{`\n        @import url('${fontUrl}');\n\n        .flex {\n          display: flex;\n          justify-content: space-between;\n        }\n\n        .stroke span {\n          position: relative;\n          color: ${textColor};\n        }\n        .stroke span::after {\n          content: attr(data-char);\n          position: absolute;\n          left: 0;\n          top: 0;\n          color: transparent;\n          z-index: -1;\n          -webkit-text-stroke-width: 3px;\n          -webkit-text-stroke-color: ${strokeColor};\n        }\n\n        .text-pressure-title {\n          color: ${textColor};\n        }\n      `}</style>\n    );\n  }, [fontFamily, fontUrl, textColor, strokeColor]);\n\n  const dynamicClassName = [className, flex ? 'flex' : '', stroke ? 'stroke' : ''].filter(Boolean).join(' ');\n\n  return (\n    <div\n      ref={containerRef}\n      style={{\n        position: 'relative',\n        width: '100%',\n        height: '100%',\n        background: 'transparent'\n      }}\n    >\n      {styleElement}\n      <h1\n        ref={titleRef}\n        className={`text-pressure-title ${dynamicClassName}`}\n        style={{\n          fontFamily,\n          textTransform: 'uppercase',\n          fontSize: fontSize,\n          lineHeight,\n          transform: `scale(1, ${scaleY})`,\n          transformOrigin: 'center top',\n          margin: 0,\n          textAlign: 'center',\n          userSelect: 'none',\n          whiteSpace: 'nowrap',\n          fontWeight: 100,\n          width: '100%'\n        }}\n      >\n        {chars.map((char, i) => (\n          <span\n            key={i}\n            ref={el => {\n              spansRef.current[i] = el;\n            }}\n            data-char={char}\n            style={{\n              display: 'inline-block',\n              color: stroke ? undefined : textColor\n            }}\n          >\n            {char}\n          </span>\n        ))}\n      </h1>\n    </div>\n  );\n};\n\nexport default TextPressure;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}