{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "VariableProximity-TS-CSS",
	"title": "VariableProximity",
	"description": "Letter styling changes continuously with pointer distance mapping.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "VariableProximity.css",
			"target": "@components/VariableProximity.css",
			"content": "@import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,100..1000&display=swap');\n\n.variable-proximity {\n  font-family: 'Roboto Flex', sans-serif;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border: 0;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "VariableProximity.tsx",
			"content": "import {\n  forwardRef,\n  useMemo,\n  useRef,\n  useEffect,\n  type MutableRefObject,\n  type RefObject,\n  type HTMLAttributes\n} from 'react';\nimport { motion } from 'motion/react';\nimport './VariableProximity.css';\n\ntype Callback = () => void;\n\nfunction useAnimationFrame(callback: Callback) {\n  useEffect(() => {\n    let frameId: number;\n    const loop = () => {\n      callback();\n      frameId = requestAnimationFrame(loop);\n    };\n    frameId = requestAnimationFrame(loop);\n    return () => cancelAnimationFrame(frameId);\n  }, [callback]);\n}\n\nfunction useMousePositionRef(containerRef: RefObject<HTMLElement>) {\n  const positionRef = useRef({ x: 0, y: 0 });\n\n  useEffect(() => {\n    const updatePosition = (x: number, y: number) => {\n      if (containerRef?.current) {\n        const rect = containerRef.current.getBoundingClientRect();\n        positionRef.current = { x: x - rect.left, y: y - rect.top };\n      } else {\n        positionRef.current = { x, y };\n      }\n    };\n\n    const handleMouseMove = (ev: MouseEvent) => updatePosition(ev.clientX, ev.clientY);\n    const handleTouchMove = (ev: TouchEvent) => {\n      const touch = ev.touches[0];\n      updatePosition(touch.clientX, touch.clientY);\n    };\n\n    window.addEventListener('mousemove', handleMouseMove);\n    window.addEventListener('touchmove', handleTouchMove);\n    return () => {\n      window.removeEventListener('mousemove', handleMouseMove);\n      window.removeEventListener('touchmove', handleTouchMove);\n    };\n  }, [containerRef]);\n\n  return positionRef;\n}\n\ninterface VariableProximityProps extends HTMLAttributes<HTMLSpanElement> {\n  label: string;\n  fromFontVariationSettings: string;\n  toFontVariationSettings: string;\n  containerRef: RefObject<HTMLElement>;\n  radius?: number;\n  falloff?: 'linear' | 'exponential' | 'gaussian';\n  className?: string;\n  onClick?: () => void;\n  style?: React.CSSProperties;\n}\n\nconst VariableProximity = forwardRef<HTMLSpanElement, VariableProximityProps>((props, ref) => {\n  const {\n    label,\n    fromFontVariationSettings,\n    toFontVariationSettings,\n    containerRef,\n    radius = 50,\n    falloff = 'linear',\n    className = '',\n    onClick,\n    style,\n    ...restProps\n  } = props;\n\n  const letterRefs = useRef<(HTMLSpanElement | null)[]>([]);\n  const interpolatedSettingsRef = useRef<string[]>([]);\n  const mousePositionRef = useMousePositionRef(containerRef);\n  const lastPositionRef = useRef<{ x: number | null; y: number | null }>({ x: null, y: null });\n\n  const parsedSettings = useMemo(() => {\n    const parseSettings = (settingsStr: string) =>\n      new Map(\n        settingsStr\n          .split(',')\n          .map(s => s.trim())\n          .map(s => {\n            const [name, value] = s.split(' ');\n            return [name.replace(/['\"]/g, ''), parseFloat(value)];\n          })\n      );\n\n    const fromSettings = parseSettings(fromFontVariationSettings);\n    const toSettings = parseSettings(toFontVariationSettings);\n\n    return Array.from(fromSettings.entries()).map(([axis, fromValue]) => ({\n      axis,\n      fromValue,\n      toValue: toSettings.get(axis) ?? fromValue\n    }));\n  }, [fromFontVariationSettings, toFontVariationSettings]);\n\n  const calculateDistance = (x1: number, y1: number, x2: number, y2: number) =>\n    Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);\n\n  const calculateFalloff = (distance: number) => {\n    const norm = Math.min(Math.max(1 - distance / radius, 0), 1);\n    switch (falloff) {\n      case 'exponential':\n        return norm ** 2;\n      case 'gaussian':\n        return Math.exp(-((distance / (radius / 2)) ** 2) / 2);\n      case 'linear':\n      default:\n        return norm;\n    }\n  };\n\n  useAnimationFrame(() => {\n    if (!containerRef?.current) return;\n    const { x, y } = mousePositionRef.current;\n    if (lastPositionRef.current.x === x && lastPositionRef.current.y === y) {\n      return;\n    }\n    lastPositionRef.current = { x, y };\n    const containerRect = containerRef.current.getBoundingClientRect();\n\n    letterRefs.current.forEach((letterRef, index) => {\n      if (!letterRef) return;\n\n      const rect = letterRef.getBoundingClientRect();\n      const letterCenterX = rect.left + rect.width / 2 - containerRect.left;\n      const letterCenterY = rect.top + rect.height / 2 - containerRect.top;\n\n      const distance = calculateDistance(\n        mousePositionRef.current.x,\n        mousePositionRef.current.y,\n        letterCenterX,\n        letterCenterY\n      );\n\n      if (distance >= radius) {\n        letterRef.style.fontVariationSettings = fromFontVariationSettings;\n        return;\n      }\n\n      const falloffValue = calculateFalloff(distance);\n      const newSettings = parsedSettings\n        .map(({ axis, fromValue, toValue }) => {\n          const interpolatedValue = fromValue + (toValue - fromValue) * falloffValue;\n          return `'${axis}' ${interpolatedValue}`;\n        })\n        .join(', ');\n\n      interpolatedSettingsRef.current[index] = newSettings;\n      letterRef.style.fontVariationSettings = newSettings;\n    });\n  });\n\n  const words = label.split(' ');\n  let letterIndex = 0;\n\n  return (\n    <span\n      ref={ref}\n      className={`${className} variable-proximity`}\n      onClick={onClick}\n      style={{ display: 'inline', ...style }}\n      {...restProps}\n    >\n      {words.map((word, wordIndex) => (\n        <span key={wordIndex} style={{ display: 'inline-block', whiteSpace: 'nowrap' }}>\n          {word.split('').map(letter => {\n            const currentLetterIndex = letterIndex++;\n            return (\n              <motion.span\n                key={currentLetterIndex}\n                ref={el => {\n                  letterRefs.current[currentLetterIndex] = el;\n                }}\n                style={{\n                  display: 'inline-block',\n                  fontVariationSettings: interpolatedSettingsRef.current[currentLetterIndex]\n                }}\n                aria-hidden=\"true\"\n              >\n                {letter}\n              </motion.span>\n            );\n          })}\n          {wordIndex < words.length - 1 && <span style={{ display: 'inline-block' }}>&nbsp;</span>}\n        </span>\n      ))}\n      <span className=\"sr-only\">{label}</span>\n    </span>\n  );\n});\n\nVariableProximity.displayName = 'VariableProximity';\nexport default VariableProximity;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12"
	]
}