{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-hover-effect",
  "title": "Text Hover Effect",
  "description": "Giant outlined word that draws in, then glows in brand colours under the pointer.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/text-hover-effect.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport {\n  motion,\n  useMotionTemplate,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface TextHoverEffectProps\n  extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n  /** The word or short phrase to draw */\n  text: string;\n  /** Outline width in SVG units (the glyphs are 100 units tall) */\n  strokeWidth?: number;\n  /** Radius of the pointer reveal, as a percentage of the drawing's size */\n  revealRadius?: number;\n  /** Seconds the outline takes to draw itself in on mount */\n  drawDuration?: number;\n}\n\nconst PAD = 4;\n\n/**\n * Oversized outlined text that draws itself in, then reveals a brand-gradient\n * version of the word inside a soft spotlight that follows the pointer.\n * Size it with a width class (`w-full`); the height follows the text. Weight\n * and font come from the className, e.g. `font-black`.\n */\nexport function TextHoverEffect({\n  text,\n  strokeWidth = 0.75,\n  revealRadius = 28,\n  drawDuration = 3,\n  className,\n  onPointerMove,\n  ...props\n}: TextHoverEffectProps) {\n  const id = useId();\n  const outlineRef = useRef<SVGTextElement>(null);\n  const reducedMotion = useReducedMotion();\n  // A rough first guess so the server render has sensible proportions; the\n  // real glyph box is measured once the font has loaded.\n  const [box, setBox] = useState({ w: text.length * 64 + PAD * 2, h: 80, x: PAD, y: 76 });\n\n  const px = useMotionValue(50);\n  const py = useMotionValue(50);\n  const spring = { stiffness: 260, damping: 32, mass: 0.6 };\n  const cx = useMotionTemplate`${useSpring(px, spring)}%`;\n  const cy = useMotionTemplate`${useSpring(py, spring)}%`;\n\n  useEffect(() => {\n    let live = true;\n    document.fonts.ready.then(() => {\n      const el = outlineRef.current;\n      if (!live || !el) return;\n      // Width from the layout box; height from the glyphs' actual ink, so\n      // caps-only words aren't padded out by empty descender space.\n      const bb = el.getBBox();\n      const css = getComputedStyle(el);\n      const ctx = document.createElement(\"canvas\").getContext(\"2d\")!;\n      ctx.font = `${css.fontStyle} ${css.fontWeight} 100px ${css.fontFamily}`;\n      const ink = ctx.measureText(text);\n      const top = ink.actualBoundingBoxAscent;\n      setBox({\n        w: bb.width + PAD * 2,\n        h: top + ink.actualBoundingBoxDescent + PAD * 2,\n        x: PAD - bb.x,\n        y: PAD + top,\n      });\n    });\n    return () => {\n      live = false;\n    };\n  }, [text]);\n\n  // Draw-in: the CSS starts the dash fully offset (motion-safe only); reading\n  // the computed style commits that state before we transition to zero.\n  useEffect(() => {\n    const el = outlineRef.current;\n    if (!el) return;\n    void getComputedStyle(el).strokeDashoffset;\n    el.style.strokeDashoffset = \"0\";\n  }, []);\n\n  const frame = { maskUnits: \"userSpaceOnUse\", x: 0, y: 0, width: \"100%\", height: \"100%\" } as const;\n  const word = (attrs: React.SVGProps<SVGTextElement>) => (\n    <text transform={`translate(${box.x} ${box.y})`} fontSize={100} {...attrs}>\n      {text}\n    </text>\n  );\n\n  return (\n    <svg\n      role=\"img\"\n      aria-label={text}\n      viewBox={`0 0 ${box.w} ${box.h}`}\n      data-slot=\"text-hover-effect\"\n      className={cn(\"group/text-hover h-auto w-full select-none font-bold\", className)}\n      onPointerMove={(event) => {\n        onPointerMove?.(event);\n        if (reducedMotion) return;\n        const rect = event.currentTarget.getBoundingClientRect();\n        px.set(((event.clientX - rect.left) / rect.width) * 100);\n        py.set(((event.clientY - rect.top) / rect.height) * 100);\n      }}\n      {...props}\n    >\n      <defs>\n        <linearGradient id={`${id}-ink`} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n          <stop offset=\"0%\" style={{ stopColor: \"var(--brand-from)\" }} />\n          <stop offset=\"50%\" style={{ stopColor: \"var(--brand-via)\" }} />\n          <stop offset=\"100%\" style={{ stopColor: \"var(--brand-to)\" }} />\n        </linearGradient>\n        <motion.radialGradient\n          id={`${id}-spot`}\n          gradientUnits=\"userSpaceOnUse\"\n          cx={cx}\n          cy={cy}\n          r={`${revealRadius}%`}\n        >\n          <stop offset=\"0%\" stopColor=\"white\" />\n          <stop offset=\"55%\" stopColor=\"white\" stopOpacity={0.55} />\n          <stop offset=\"100%\" stopColor=\"white\" stopOpacity={0} />\n        </motion.radialGradient>\n        <mask id={`${id}-spot-mask`} {...frame}>\n          <rect width=\"100%\" height=\"100%\" fill={`url(#${id}-spot)`} />\n        </mask>\n        {/* Hides everything inside the glyphs, so variable fonts' overlapping\n            contours never show through the outline. */}\n        <mask id={`${id}-outside`} {...frame}>\n          <rect width=\"100%\" height=\"100%\" fill=\"white\" />\n          {word({ fill: \"black\" })}\n        </mask>\n      </defs>\n\n      <g mask={`url(#${id}-outside)`}>\n        {word({\n          ref: outlineRef,\n          strokeWidth: strokeWidth * 2,\n          style: { transitionDuration: `${drawDuration}s` },\n          className:\n            \"fill-none stroke-foreground/35 transition-[stroke-dashoffset] ease-in-out [stroke-dasharray:1000] motion-safe:[stroke-dashoffset:1000] motion-reduce:transition-none\",\n          onTransitionEnd: (event) => {\n            event.currentTarget.style.strokeDasharray = \"none\";\n          },\n        })}\n      </g>\n\n      <g\n        mask={`url(#${id}-spot-mask)`}\n        className=\"opacity-0 transition-opacity duration-500 group-hover/text-hover:opacity-100 motion-reduce:duration-150\"\n      >\n        {word({ fill: `url(#${id}-ink)`, fillOpacity: 0.3 })}\n        <g mask={`url(#${id}-outside)`}>\n          {word({ fill: \"none\", stroke: `url(#${id}-ink)`, strokeWidth: strokeWidth * 3 })}\n        </g>\n      </g>\n    </svg>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/text-hover-effect.tsx"
    }
  ],
  "type": "registry:ui"
}