{ "name": "text-repel", "type": "registry:ui", "dependencies": [], "registryDependencies": [], "description": "Component for text-repel", "files": [ { "path": "components/ui/text-repel.tsx", "content": "\"use client\";\n\nimport { cn } from \"@workspace/ui/lib/utils\";\nimport {\n motion,\n useMotionValue,\n useSpring,\n useTransform,\n type MotionValue,\n} from \"framer-motion\";\nimport { useEffect, useRef } from \"react\";\n\ninterface TextRepelProps {\n /** The text to display */\n text: string;\n /** Additional CSS classes for the container */\n className?: string;\n /** CSS classes applied to each letter */\n letterClassName?: string;\n /** Cursor influence radius in pixels */\n radius?: number;\n /** Maximum displacement strength in pixels */\n strength?: number;\n /** Interaction mode — push letters away or pull them toward the cursor */\n mode?: \"repel\" | \"attract\";\n /** Spring stiffness — higher = snappier return */\n stiffness?: number;\n /** Spring damping — lower = bouncier return */\n damping?: number;\n /** Spring mass — higher = heavier feel */\n mass?: number;\n}\n\nfunction RepelLetter({\n letter,\n mouseX,\n mouseY,\n radius,\n strength,\n mode,\n stiffness,\n damping,\n mass,\n className,\n}: {\n letter: string;\n mouseX: MotionValue;\n mouseY: MotionValue;\n radius: number;\n strength: number;\n mode: \"repel\" | \"attract\";\n stiffness: number;\n damping: number;\n mass: number;\n className?: string;\n}) {\n const ref = useRef(null);\n const originX = useRef(0);\n const originY = useRef(0);\n\n const x = useMotionValue(0);\n const y = useMotionValue(0);\n const springX = useSpring(x, { stiffness, damping, mass });\n const springY = useSpring(y, { stiffness, damping, mass });\n\n // Subtle tilt proportional to horizontal displacement\n const rotate = useTransform(springX, (v) => v * 0.3);\n\n // Capture original position relative to the container\n useEffect(() => {\n const capture = () => {\n if (!ref.current) return;\n const container = ref.current.closest(\"[data-text-repel]\");\n if (!container) return;\n const cr = container.getBoundingClientRect();\n const lr = ref.current.getBoundingClientRect();\n originX.current = lr.left - cr.left + lr.width / 2;\n originY.current = lr.top - cr.top + lr.height / 2;\n };\n\n const raf = requestAnimationFrame(capture);\n window.addEventListener(\"resize\", capture);\n return () => {\n cancelAnimationFrame(raf);\n window.removeEventListener(\"resize\", capture);\n };\n }, []);\n\n // React to cursor position changes via motion value subscriptions\n useEffect(() => {\n const update = () => {\n const mx = mouseX.get();\n const my = mouseY.get();\n const dx = originX.current - mx;\n const dy = originY.current - my;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n if (distance < radius && distance > 0) {\n // Quadratic falloff for natural-feeling force\n const force = ((1 - distance / radius) ** 2) * strength;\n const angle = Math.atan2(dy, dx);\n const dir = mode === \"attract\" ? -1 : 1;\n x.set(Math.cos(angle) * force * dir);\n y.set(Math.sin(angle) * force * dir);\n } else {\n x.set(0);\n y.set(0);\n }\n };\n\n const unsub1 = mouseX.on(\"change\", update);\n const unsub2 = mouseY.on(\"change\", update);\n return () => {\n unsub1();\n unsub2();\n };\n }, [mouseX, mouseY, radius, strength, mode, x, y]);\n\n if (letter === \" \") {\n return ;\n }\n\n return (\n \n {letter}\n \n );\n}\n\nexport function TextRepel({\n text,\n className,\n letterClassName,\n radius = 120,\n strength = 45,\n mode = \"repel\",\n stiffness = 180,\n damping = 14,\n mass = 0.4,\n}: TextRepelProps) {\n const containerRef = useRef(null);\n const mouseX = useMotionValue(-9999);\n const mouseY = useMotionValue(-9999);\n\n return (\n {\n const rect = containerRef.current?.getBoundingClientRect();\n if (!rect) return;\n mouseX.set(e.clientX - rect.left);\n mouseY.set(e.clientY - rect.top);\n }}\n onMouseLeave={() => {\n mouseX.set(-9999);\n mouseY.set(-9999);\n }}\n aria-label={text}\n >\n {text.split(\"\").map((letter, i) => (\n \n ))}\n \n );\n}\n", "type": "registry:ui" } ] }