{
  "title": "Draw Line Text",
  "description": "A smooth text effect that draws characters as strokes and fills them in-perfect for headers and highlights.",
  "name": "motion-draw-line-text",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "type": "registry:ui",
  "files": [
    {
      "path": "components/paceui/motion/draw-line-text.tsx",
      "type": "registry:ui",
      "content": "import { type ComponentProps, useEffect, useRef, useState } from \"react\";\n\nimport { animate, stagger } from \"motion/react\";\n\ntype DrawTextProps = {\n    afterFill?: boolean;\n    color?: string;\n    fontSize?: number;\n    letterSpacing?: number;\n    oneByOne?: boolean;\n    strokeWidth?: number;\n    text: string;\n    wordSpacing?: number;\n} & ComponentProps<\"svg\">;\n\nexport const DrawLineText = ({\n    text,\n    oneByOne = true,\n    afterFill = true,\n    color = \"black\",\n    fontSize = 40,\n    wordSpacing = 10,\n    strokeWidth = 1,\n    letterSpacing = 0,\n    ...props\n}: DrawTextProps) => {\n    const wrapperRef = useRef<SVGSVGElement | null>(null);\n\n    // We use React state to store the exact layout instead of mutating the DOM directly\n    const [layout, setLayout] = useState<{\n        width: number;\n        height: number;\n        chars: { x: number; length: number; char: string; isSpace: boolean }[];\n    } | null>(null);\n\n    // Phase 1: Wait for fonts, then measure the layout\n    useEffect(() => {\n        const svgElement = wrapperRef.current;\n        if (!svgElement) return;\n\n        let isCancelled = false;\n\n        // document.fonts.ready guarantees custom fonts have loaded before measuring\n        document.fonts.ready.then(() => {\n            if (isCancelled) return;\n\n            const children = Array.from(svgElement.querySelectorAll(\"text\"));\n            let totalWidth = 0;\n            let maxHeight = fontSize * 1.2;\n            const charsData: { x: number; length: number; char: string; isSpace: boolean }[] = [];\n\n            children.forEach((el, index) => {\n                const char = text[index];\n                const isSpace = char === \" \";\n                const elementWidth = el.getComputedTextLength();\n                const rectHeight = el.getBoundingClientRect().height;\n\n                if (rectHeight > maxHeight) maxHeight = rectHeight;\n\n                const actualWidth = isSpace ? wordSpacing : elementWidth;\n                const x = totalWidth;\n\n                // Simulates path length for text outlines\n                const length = actualWidth * 8;\n\n                totalWidth += actualWidth + (index !== children.length - 1 ? letterSpacing : 0);\n\n                charsData.push({ x, length, char, isSpace });\n            });\n\n            // Trigger re-render with actual positioned layout\n            setLayout({\n                width: totalWidth,\n                height: maxHeight,\n                chars: charsData,\n            });\n        });\n\n        return () => {\n            isCancelled = true;\n        };\n    }, [text, fontSize, wordSpacing, letterSpacing]);\n\n    // Phase 2: Animate only once the layout state is established\n    useEffect(() => {\n        if (!layout) return;\n\n        const svgElement = wrapperRef.current;\n        if (!svgElement) return;\n\n        const textChildren = Array.from(svgElement.querySelectorAll(\"text\")).filter((_, i) => !layout.chars[i].isSpace);\n\n        if (textChildren.length === 0) return;\n\n        const sequence: any[] = [\n            [\n                textChildren,\n                { strokeDashoffset: 0 },\n                { duration: 2.5, ease: \"linear\", delay: oneByOne ? stagger(0.8) : 0 },\n            ],\n        ];\n\n        if (afterFill) {\n            sequence.push([\n                textChildren,\n                { fillOpacity: 1 },\n                {\n                    duration: 0.6,\n                    ease: \"easeIn\",\n                    delay: stagger(0.2 / textChildren.length, { from: \"center\" }),\n                },\n            ]);\n        }\n\n        const animation = animate(sequence);\n\n        return () => {\n            animation.stop();\n        };\n    }, [layout, afterFill, oneByOne]);\n\n    // If layout isn't calculated yet, render a hidden \"measuring tape\" version\n    if (!layout) {\n        return (\n            <svg\n                {...props}\n                ref={wrapperRef}\n                style={{\n                    opacity: 0,\n                    position: \"absolute\", // prevents layout shifts\n                    pointerEvents: \"none\",\n                    visibility: \"hidden\",\n                }}>\n                {text.split(\"\").map((char, i) => (\n                    <text key={i} y={fontSize} style={{ fontSize, strokeWidth: `${strokeWidth}px` }}>\n                        {char}\n                    </text>\n                ))}\n            </svg>\n        );\n    }\n\n    // Render the final, visible component ready for animation\n    return (\n        <svg\n            {...props}\n            ref={wrapperRef}\n            width={layout.width}\n            height={layout.height}\n            style={{\n                userSelect: \"none\",\n                overflow: \"visible\",\n                ...props.style,\n            }}>\n            {layout.chars.map((charData, i) => (\n                <text\n                    key={i}\n                    x={charData.x}\n                    y={fontSize}\n                    style={{\n                        stroke: color,\n                        fill: color,\n                        fillOpacity: 0,\n                        fontSize: fontSize,\n                        strokeWidth: `${strokeWidth}px`,\n                        strokeDasharray: `${charData.length}px`,\n                        strokeDashoffset: `${charData.length}px`, // Starts fully hidden\n                    }}>\n                    {charData.char}\n                </text>\n            ))}\n        </svg>\n    );\n};\n"
    }
  ],
  "mcp": {
    "description": "Provides refined UI interactions and accessible controls. A smooth text effect that draws characters as strokes and fills them in-perfect for headers and highlights. Configurable props include: {; afterFill?: boolean;; color?: string;; fontSize?: number;. Relies on motion. Optimal for building reusable interface elements and user input flows.",
    "tags": [
      "component",
      "motion",
      "animation"
    ]
  }
}