{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sensitive-text",
  "type": "registry:ui",
  "title": "Sensitive Text",
  "description": "Interactive text that subtly compresses or stretches characters based on cursor proximity.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/sensitive-text.tsx",
      "content": "\"use client\";\n\nimport React, { useRef, useEffect, useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SensitiveTextProps {\n  children: string;\n  className?: string;\n  containerClassName?: string;\n  maxStretch?: number;\n  sensitivity?: number;\n  easing?: number;\n  verticalRange?: number;\n  falloffPower?: number;\n  variant?: \"compress\" | \"stretch\";\n  style?: React.CSSProperties;\n}\n\nconst SensitiveText: React.FC<SensitiveTextProps> = ({\n  children,\n  className = \"\",\n  containerClassName = \"\",\n  maxStretch = 0.5,\n  sensitivity = 0.5,\n  easing = 10,\n  verticalRange = 100,\n  falloffPower = 2,\n  variant = \"compress\",\n  style = {},\n}) => {\n  const textRef = useRef<HTMLSpanElement>(null);\n  const spansRef = useRef<(HTMLSpanElement | null)[]>([]);\n  const mouseRef = useRef({ x: 0, y: 0 });\n  const cursorRef = useRef({ x: 0, y: 0 });\n  const scalesRef = useRef<number[]>([]);\n\n  const chars = useMemo(() => children.split(\"\"), [children]);\n\n  useEffect(() => {\n    scalesRef.current = chars.map(() => 1);\n\n    const handleMouseMove = (e: MouseEvent) => {\n      cursorRef.current.x = e.clientX;\n      cursorRef.current.y = e.clientY;\n    };\n\n    const handleTouchMove = (e: TouchEvent) => {\n      const touch = e.touches[0];\n      cursorRef.current.x = touch.clientX;\n      cursorRef.current.y = touch.clientY;\n    };\n\n    window.addEventListener(\"mousemove\", handleMouseMove);\n    window.addEventListener(\"touchmove\", handleTouchMove, { passive: true });\n\n    return () => {\n      window.removeEventListener(\"mousemove\", handleMouseMove);\n      window.removeEventListener(\"touchmove\", handleTouchMove);\n    };\n  }, [chars]);\n\n  useEffect(() => {\n    let rafId: number;\n\n    const animate = () => {\n      mouseRef.current.x += (cursorRef.current.x - mouseRef.current.x) / easing;\n      mouseRef.current.y += (cursorRef.current.y - mouseRef.current.y) / easing;\n\n      if (!textRef.current) {\n        rafId = requestAnimationFrame(animate);\n        return;\n      }\n\n      const textRect = textRef.current.getBoundingClientRect();\n\n      const isInHorizontalBounds =\n        mouseRef.current.x >= textRect.left &&\n        mouseRef.current.x <= textRect.right;\n\n      const isInVerticalBounds =\n        mouseRef.current.y >= textRect.top &&\n        mouseRef.current.y <= textRect.bottom + verticalRange;\n\n      const isInBounds = isInHorizontalBounds && isInVerticalBounds;\n\n      spansRef.current.forEach((span, index) => {\n        if (!span) return;\n\n        const rect = span.getBoundingClientRect();\n        const charCenterX = rect.left + rect.width / 2;\n\n        const distX = Math.abs(mouseRef.current.x - charCenterX);\n        const maxDist = textRect.width * sensitivity;\n\n        let targetScale = 1;\n\n        if (isInBounds) {\n          const proximity = Math.max(0, 1 - distX / maxDist);\n\n          if (variant === \"compress\") {\n            targetScale = 1 - proximity ** falloffPower * maxStretch;\n          } else {\n            targetScale = 1 + proximity ** falloffPower * maxStretch;\n          }\n        }\n\n        const currentScale = scalesRef.current[index];\n        const newScale = currentScale + (targetScale - currentScale) * 0.15;\n        scalesRef.current[index] = newScale;\n\n        span.style.transform = `scaleY(${newScale})`;\n      });\n\n      rafId = requestAnimationFrame(animate);\n    };\n\n    animate();\n    return () => cancelAnimationFrame(rafId);\n  }, [maxStretch, sensitivity, easing, verticalRange, falloffPower, variant]);\n\n  return (\n    <span\n      ref={textRef}\n      className={cn(containerClassName)}\n      style={{\n        ...style,\n        display: \"inline-flex\",\n        alignItems: \"flex-start\",\n        lineHeight: 0,\n        verticalAlign: \"top\",\n      }}\n    >\n      {chars.map((char, i) => (\n        <span\n          key={i}\n          ref={(el) => {\n            spansRef.current[i] = el;\n          }}\n          className={cn(className)}\n          style={{\n            display: \"inline-block\",\n            whiteSpace: char === \" \" ? \"pre\" : \"normal\",\n            transformOrigin: \"top center\",\n            lineHeight: 1,\n            verticalAlign: \"top\",\n          }}\n        >\n          {char === \" \" ? \"\\u00A0\" : char}\n        </span>\n      ))}\n    </span>\n  );\n};\n\nexport default SensitiveText;\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}"
    }
  ]
}
