Hover Weight
Installation
npx shadcn@latest add https://uibrik.com/r/hover-weight.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/hover-weight/hover-weight.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"; // 01 — Letters near the pointer grow bolder and settle back once it moves // on. Pressing Change sweeps a virtual pointer across the line. const AWAY = -9999; export function HoverWeight({ play, text = "Move closer", minWeight = 400, maxWeight = 700, radius = 60, sweepDuration = 0.9, }: EffectProps & { text?: string; /** Weight away from the pointer. */ minWeight?: number; /** Weight right under the pointer. */ maxWeight?: number; /** Reach of the pointer 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 cursor-default")} onPointerMove={(e) => pointer.set(e.clientX - e.currentTarget.getBoundingClientRect().left)} onPointerLeave={() => pointer.set(AWAY)} > {Array.from(text).map((ch, i) => ( <GravityLetter key={i} ch={ch} pointer={pointer} minWeight={minWeight} maxWeight={maxWeight} radius={radius} /> ))} </p> ); } // offsetLeft ignores the weight change of neighbours mid-frame closely // enough, and it is read from layout so no transform feeds back into it. function GravityLetter({ ch, pointer, minWeight, maxWeight, radius, }: { ch: string; pointer: MotionValue<number>; minWeight: number; maxWeight: number; radius: number; }) { const ref = useRef<HTMLSpanElement>(null); const target = useTransform(pointer, (p) => { const el = ref.current; if (!el) return minWeight; const d = Math.abs(p - (el.offsetLeft + el.offsetWidth / 2)); return minWeight + (maxWeight - minWeight) * Math.max(0, 1 - d / radius); }); const weight = useSpring(target, { stiffness: 300, damping: 30 }); return ( <motion.span ref={ref} aria-hidden style={{ fontWeight: weight }}> {glyph(ch)} </motion.span> ); }
Adds the files to
components/uibrik/hover-weight and installs motion. With the @uibrik namespace set up: npx shadcn@latest add @uibrik/hover-weight.Props
HoverWeight- text
- Type string
- "Move closer"
- The copy to animate.
- minWeight
- Type number
- 400
- Weight away from the pointer.
- maxWeight
- Type number
- 700
- Weight right under the pointer.
- radius
- Type number
- 60
- Reach of the pointer.
- sweepDuration
- Type number
- 0.9
- Replay sweep across the line.
- playrequired
- Type number
- –
- Each change plays the effect again; 0 shows its resting state.