{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "text-hover-effect", "title": "Text Hover Effect", "description": "Text that reveals a cursor-following gradient and stroke animation on hover. | ホバー時にカーソル追従のグラデーションとストロークアニメーションで表示されるテキスト。", "dependencies": [ "motion" ], "files": [ { "path": "components/text/text-hover-effect.tsx", "content": "\"use client\";\nimport { useRef, useEffect, useState } from \"react\";\nimport { motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\n/** Base class for stroke-outline text layers (outline + motion stroke). */\nconst strokeOutlineTextClass =\n \"fill-transparent stroke-muted-foreground/50 font-[helvetica] text-[5rem] font-bold\";\n/** Base class for the gradient-masked text layer (stroke from gradient, no fill). */\nconst gradientTextClass =\n \"fill-transparent font-[helvetica] text-[5rem] font-bold\";\n\n/**\n * Props for the Text Hover Effect component.\n * @see TextHoverEffect\n */\nexport interface TextHoverEffectProps {\n /** The text to display. Revealed with a cursor-following gradient and stroke animation on hover. */\n text: string;\n /**\n * Duration (seconds) for the radial mask to follow the cursor.\n * Use 0 for no transition (instant follow). Default: 0.\n */\n duration?: number;\n /** Optional CSS class for the root SVG container (e.g. width, height, margin). */\n className?: string;\n /** Optional CSS class merged into all text elements (e.g. font size, font family, stroke). */\n textClassName?: string;\n}\n\n/**\n * Text that reveals a cursor-following gradient and stroke animation on hover.\n * Use className for the SVG container; use textClassName to style the text (font, size, stroke).\n */\nexport const TextHoverEffect = ({\n text,\n duration,\n className,\n textClassName,\n}: TextHoverEffectProps) => {\n const svgRef = useRef(null);\n const [cursor, setCursor] = useState({ x: 0, y: 0 });\n const [hovered, setHovered] = useState(false);\n const [maskPosition, setMaskPosition] = useState({ cx: \"50%\", cy: \"50%\" });\n\n useEffect(() => {\n if (svgRef.current && cursor.x !== null && cursor.y !== null) {\n const svgRect = svgRef.current.getBoundingClientRect();\n const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100;\n const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100;\n setMaskPosition({\n cx: `${cxPercentage}%`,\n cy: `${cyPercentage}%`,\n });\n }\n }, [cursor]);\n\n return (\n setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n onMouseMove={(e) => setCursor({ x: e.clientX, y: e.clientY })}\n className={cn(\"select-none\", className)}\n >\n \n \n {hovered && (\n <>\n \n \n \n \n \n \n )}\n \n\n \n \n \n \n \n \n \n \n \n {text}\n \n \n {text}\n \n \n {text}\n \n \n );\n};\n", "type": "registry:ui" } ], "type": "registry:ui" }