{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "VariableProximity-TS-TW",
	"title": "VariableProximity",
	"description": "Letter styling changes continuously with pointer distance mapping.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "VariableProximity/VariableProximity.tsx",
			"content": "import {\n  forwardRef,\n  useMemo,\n  useRef,\n  useEffect,\n  type MutableRefObject,\n  type CSSProperties,\n  type HTMLAttributes\n} from 'react';\nimport { motion } from 'motion/react';\n\nfunction useAnimationFrame(callback: () => void) {\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: MutableRefObject<HTMLElement | null>) {\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: MutableRefObject<HTMLElement | null>;\n  radius?: number;\n  falloff?: 'linear' | 'exponential' | 'gaussian';\n  className?: string;\n  onClick?: () => void;\n  style?: 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      onClick={onClick}\n      style={{\n        display: 'inline',\n        fontFamily: '\"Roboto Flex\", sans-serif',\n        ...style\n      }}\n      className={className}\n      {...restProps}\n    >\n      {words.map((word, wordIndex) => (\n        <span key={wordIndex} className=\"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 className=\"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"
	]
}