{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "variable-text", "type": "registry:ui", "title": "Variable Text", "description": "Cursor acts as a focal plane — characters near the pointer sharpen (heavier weight, slight lift, full opacity) while distant characters soften. Depth-of-field for typography.", "dependencies": [ "motion" ], "registryDependencies": [], "files": [ { "path": "registry/ruixenui/variable-text.tsx", "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n motion,\n useMotionValue,\n useSpring,\n useTransform,\n type MotionValue,\n} from \"motion/react\";\nimport { useEffect, useRef } from \"react\";\n\n/* ═══════════════════════════════════════════════════════════\n Variable Text — cursor is a focal plane. Characters near\n the pointer sharpen: heavier weight, slight lift, full\n opacity, subtle scale. Characters far from focus soften:\n lighter weight, smaller, translucent. The effect is\n photographic depth-of-field applied to typography.\n\n Single proximity spring per character → four derived\n motion values (fontWeight, scale, y, opacity). Geist's\n variable wght axis (100–900) enables sub-pixel-smooth\n weight transitions — fontWeight: 523.7 just works.\n ═══════════════════════════════════════════════════════════ */\n\ntype TextElement = \"p\" | \"span\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n\ninterface VariableTextProps {\n children: string;\n as?: TextElement;\n className?: string;\n}\n\nconst SPRING = { stiffness: 400, damping: 28 };\nconst THRESHOLD = 100;\n\nfunction Char({\n char,\n mouseX,\n mouseY,\n}: {\n char: string;\n mouseX: MotionValue;\n mouseY: MotionValue;\n}) {\n const ref = useRef(null);\n const pos = useRef({ x: 0, y: 0 });\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const sync = () => {\n pos.current = {\n x: el.offsetLeft + el.offsetWidth / 2,\n y: el.offsetTop + el.offsetHeight / 2,\n };\n };\n sync();\n window.addEventListener(\"resize\", sync);\n return () => window.removeEventListener(\"resize\", sync);\n }, []);\n\n /* ── single proximity value: +1 (touching) → 0 (edge) → −1 (far) ── */\n const rawProximity = useTransform(mouseX, (mx) => {\n if (mx < -9000) return 0;\n const my = mouseY.get();\n const dx = mx - pos.current.x;\n const dy = my - pos.current.y;\n const dist = Math.sqrt(dx * dx + dy * dy);\n\n if (dist < THRESHOLD) {\n const ratio = 1 - dist / THRESHOLD;\n return ratio * ratio; // quadratic ease — 0→1\n }\n return Math.max(-1, -((dist - THRESHOLD) / THRESHOLD)); // 0→−1\n });\n\n const proximity = useSpring(rawProximity, SPRING);\n\n /* ── four derived properties from one spring ── */\n const fontWeight = useTransform(proximity, (p) =>\n p >= 0 ? 400 + p * 400 : 400 + p * 150,\n ); // near: 400→800, far: 400→250\n\n const scale = useTransform(proximity, (p) =>\n p >= 0 ? 1 + p * 0.25 : 1 + p * 0.07,\n ); // near: 1→1.25, far: 1→0.93\n\n const y = useTransform(proximity, (p) => (p > 0 ? p * -6 : 0));\n // near: lift up to −6px\n\n const opacity = useTransform(proximity, (p) => (p >= 0 ? 1 : 1 + p * 0.55)); // far: 1→0.45\n\n return (\n \n {char === \" \" ? \"\\u00A0\" : char}\n \n );\n}\n\nexport default function VariableText({\n children,\n as: Tag = \"p\",\n className,\n}: VariableTextProps) {\n const mouseX = useMotionValue(-9999);\n const mouseY = useMotionValue(-9999);\n\n return (\n ) => {\n const rect = e.currentTarget.getBoundingClientRect();\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 >\n {Array.from(children).map((char, i) => (\n \n ))}\n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/variable-text.tsx" } ] }