Pull Toward Cursor
Installation
npx shadcn@latest add https://uibrik.com/r/pull-to-cursor.jsonInstall the packages
npm install motionAdd the cn helper
Imports
cnfrom@/lib/utils, whichshadcn initcreates. Add it by hand without shadcn.Copy the source into your project
components/uibrik/pull-to-cursor/pull-to-cursor.tsx"use client"; import { cn } from "@/lib/utils"; import { animate, motion, useMotionValue, useSpring, useTransform, type MotionValue, } from "motion/react"; import { useEffect, useRef } from "react"; import { TEXT, glyph, type EffectProps } from "../_text-effects/shared"; // 12 — Letters lean toward the pointer, so spacing tightens where it hovers // and opens again as it moves on. Pressing Change sweeps it across. const AWAY = -9999; export function PullToCursor({ play, text = "Drawn together", strength = 0.18, radius = 110, sweepDuration = 0.9, }: EffectProps & { text?: string; /** How far letters lean toward the pointer, as a share of the distance. */ strength?: number; /** Reach of the pull in px. */ radius?: number; /** Seconds for the replay sweep across the line. */ sweepDuration?: number; }) { const pointer = useMotionValue(AWAY); const rootRef = useRef<HTMLParagraphElement>(null); useEffect(() => { const width = rootRef.current?.offsetWidth ?? 0; if (!play || !width) return; const controls = animate(pointer, [0, width], { duration: sweepDuration, ease: "easeInOut", onComplete: () => pointer.set(AWAY), }); return () => controls.stop(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [play, pointer]); return ( <p ref={rootRef} aria-label={text} className={cn(TEXT, "relative flex cursor-default")} onPointerMove={(e) => pointer.set(e.clientX - e.currentTarget.getBoundingClientRect().left)} onPointerLeave={() => pointer.set(AWAY)} > {Array.from(text).map((ch, i) => ( <MagnetLetter key={i} ch={ch} pointer={pointer} strength={strength} radius={radius} /> ))} </p> ); } // offsetLeft ignores transforms, so the rest position is read straight from // layout while the letter itself is displaced. function MagnetLetter({ ch, pointer, strength, radius, }: { ch: string; pointer: MotionValue<number>; strength: number; radius: number; }) { const ref = useRef<HTMLSpanElement>(null); const target = useTransform(pointer, (p) => { const el = ref.current; if (!el) return 0; const d = p - (el.offsetLeft + el.offsetWidth / 2); return d * strength * Math.max(0, 1 - Math.abs(d) / radius); }); const x = useSpring(target, { stiffness: 300, damping: 30 }); return ( <motion.span ref={ref} aria-hidden className="inline-block" style={{ x }}> {glyph(ch)} </motion.span> ); }
Adds the files to
components/uibrik/pull-to-cursor and installs motion. With the @uibrik namespace set up: npx shadcn@latest add @uibrik/pull-to-cursor.Props
PullToCursor- text
- Type string
- "Drawn together"
- The copy to animate.
- strength
- Type number
- 0.18
- How far letters lean toward the pointer.
- radius
- Type number
- 110
- Reach of the pull.
- sweepDuration
- Type number
- 0.9
- Replay sweep across the line.
- playrequired
- Type number
- –
- Each change plays the effect again; 0 shows its resting state.