{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "StrokeText-JS-TW",
	"title": "StrokeText",
	"description": "Outlined letterforms draw themselves on, then flood with fill.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "StrokeText/StrokeText.jsx",
			"content": "import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\n\nif (typeof window !== 'undefined') {\n  gsap.registerPlugin(ScrollTrigger);\n}\n\nconst DEFAULT_TEXT = 'Draw Attention';\n\nconst StrokeText = ({\n  text = DEFAULT_TEXT,\n  strokeColor = '#A78BFA',\n  fillColor = '#F8FAFC',\n  strokeWidth = 1.4,\n  drawDuration = 1.6,\n  fillDelay = 0.2,\n  stagger = 0.05,\n  ease = 'power2.out',\n  trigger = 'mount',\n  fillMode = 'wipe',\n  fontSize = 128,\n  fontWeight = 800,\n  letterSpacing = -4,\n  reverse = false,\n  className = '',\n  style = {}\n}) => {\n  const rootRef = useRef(null);\n  const strokeTextRef = useRef(null);\n  const wipeRectRef = useRef(null);\n\n  const [box, setBox] = useState(null);\n\n  const rawId = useId();\n  const wipeId = `stroke-text-wipe-${rawId.replace(/[^a-zA-Z0-9_-]/g, '')}`;\n\n  const characters = useMemo(() => Array.from(String(text ?? '')), [text]);\n\n  const dash = Math.max(fontSize * 7, 200);\n\n  const fontStyle = useMemo(\n    () => ({\n      fontSize: `${fontSize}px`,\n      fontWeight,\n      letterSpacing: `${letterSpacing}px`\n    }),\n    [fontSize, fontWeight, letterSpacing]\n  );\n\n  useLayoutEffect(() => {\n    const node = strokeTextRef.current;\n    if (!node) return undefined;\n\n    let cancelled = false;\n\n    const measure = () => {\n      if (cancelled || !strokeTextRef.current) return;\n      let bbox;\n      try {\n        bbox = strokeTextRef.current.getBBox();\n      } catch {\n        return;\n      }\n      if (!bbox || !bbox.width) return;\n\n      const pad = Math.max(Number(strokeWidth) || 1, fontSize * 0.1);\n      const next = {\n        x: bbox.x - pad,\n        y: bbox.y - pad,\n        width: bbox.width + pad * 2,\n        height: bbox.height + pad * 2\n      };\n\n      setBox(prev =>\n        prev &&\n        Math.abs(prev.x - next.x) < 0.5 &&\n        Math.abs(prev.width - next.width) < 0.5 &&\n        Math.abs(prev.y - next.y) < 0.5\n          ? prev\n          : next\n      );\n    };\n\n    measure();\n    if (typeof document !== 'undefined' && document.fonts?.ready) {\n      document.fonts.ready.then(measure).catch(() => {});\n    }\n\n    return () => {\n      cancelled = true;\n    };\n  }, [characters, fontSize, fontWeight, letterSpacing, strokeWidth]);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (typeof window === 'undefined' || !root || !box) return undefined;\n\n    const strokes = gsap.utils.toArray(root.querySelectorAll('[data-stroke-char]'));\n    const fills = gsap.utils.toArray(root.querySelectorAll('[data-fill-char]'));\n    const wipe = wipeRectRef.current;\n    if (!strokes.length) return undefined;\n\n    const fillEnabled = fillMode !== 'none';\n    const useWipe = fillEnabled && fillMode === 'wipe';\n    const fillDuration = Math.max(0.4, drawDuration * 0.5);\n    const staggerConfig = reverse ? { each: stagger, from: 'end' } : stagger;\n    const targets = [...strokes, ...fills, wipe].filter(Boolean);\n\n    const setStart = () => {\n      gsap.killTweensOf(targets);\n      gsap.set(strokes, { strokeDasharray: dash, strokeDashoffset: dash });\n      gsap.set(fills, { opacity: useWipe ? 1 : 0 });\n      if (wipe) gsap.set(wipe, { attr: { width: 0 } });\n    };\n\n    const setEnd = () => {\n      gsap.killTweensOf(targets);\n      gsap.set(strokes, { strokeDasharray: dash, strokeDashoffset: 0 });\n      gsap.set(fills, { opacity: fillEnabled ? 1 : 0 });\n      if (wipe) gsap.set(wipe, { attr: { width: fillEnabled ? box.width : 0 } });\n    };\n\n    const prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n    if (prefersReducedMotion) {\n      setEnd();\n      return () => gsap.killTweensOf(targets);\n    }\n\n    const build = () => {\n      setStart();\n      const tl = gsap.timeline({\n        paused: true,\n        repeat: trigger === 'loop' ? -1 : 0,\n        repeatDelay: trigger === 'loop' ? 0.9 : 0,\n        defaults: { overwrite: 'auto' }\n      });\n\n      tl.to(strokes, { strokeDashoffset: 0, duration: drawDuration, ease, stagger: staggerConfig }, 0);\n\n      if (useWipe && wipe) {\n        tl.to(\n          wipe,\n          { attr: { width: box.width }, duration: fillDuration, ease: 'power2.inOut' },\n          drawDuration + fillDelay\n        );\n      } else if (fillEnabled) {\n        tl.to(\n          fills,\n          { opacity: 1, duration: fillDuration, ease: 'power2.out', stagger: staggerConfig },\n          drawDuration + fillDelay\n        );\n      }\n\n      return tl;\n    };\n\n    let timeline = null;\n    let scrollTrigger = null;\n    let removeHover = null;\n\n    if (trigger === 'hover') {\n      setEnd();\n      const play = () => {\n        timeline?.kill();\n        timeline = build();\n        timeline.play(0);\n      };\n      root.addEventListener('pointerenter', play);\n      removeHover = () => root.removeEventListener('pointerenter', play);\n    } else {\n      timeline = build();\n      if (trigger === 'scroll') {\n        scrollTrigger = ScrollTrigger.create({\n          trigger: root,\n          start: 'top 82%',\n          once: true,\n          onEnter: () => timeline?.play(0)\n        });\n      } else {\n        timeline.play(0);\n      }\n    }\n\n    return () => {\n      removeHover?.();\n      scrollTrigger?.kill();\n      timeline?.kill();\n      gsap.killTweensOf(targets);\n    };\n  }, [box, dash, drawDuration, fillDelay, stagger, ease, trigger, fillMode, reverse]);\n\n  const viewBox = box ? `${box.x} ${box.y} ${box.width} ${box.height}` : `0 ${-fontSize} 600 ${fontSize * 1.3}`;\n\n  return (\n    <span\n      ref={rootRef}\n      className={`block w-full leading-[0] ${trigger === 'hover' ? 'cursor-pointer' : ''} ${className}`.trim()}\n      style={style}\n      role=\"img\"\n      aria-label={String(text ?? '')}\n    >\n      <svg\n        className=\"block w-full\"\n        style={{ height: `${Math.round(fontSize * 1.3)}px` }}\n        viewBox={viewBox}\n        preserveAspectRatio=\"xMidYMid meet\"\n        aria-hidden=\"true\"\n      >\n        {fillMode === 'wipe' && box && (\n          <defs>\n            <clipPath id={wipeId} clipPathUnits=\"userSpaceOnUse\">\n              <rect ref={wipeRectRef} x={box.x} y={box.y} width=\"0\" height={box.height} />\n            </clipPath>\n          </defs>\n        )}\n\n        <text\n          ref={strokeTextRef}\n          className=\"select-none\"\n          x=\"0\"\n          y=\"0\"\n          fill=\"none\"\n          stroke={strokeColor}\n          strokeWidth={strokeWidth}\n          strokeLinejoin=\"round\"\n          strokeLinecap=\"round\"\n          style={fontStyle}\n        >\n          {characters.map((char, index) => (\n            <tspan data-stroke-char key={`s-${index}`}>\n              {char}\n            </tspan>\n          ))}\n        </text>\n\n        <text\n          className=\"select-none\"\n          x=\"0\"\n          y=\"0\"\n          fill={fillColor}\n          stroke=\"none\"\n          style={fontStyle}\n          clipPath={fillMode === 'wipe' && box ? `url(#${wipeId})` : undefined}\n        >\n          {characters.map((char, index) => (\n            <tspan data-fill-char key={`f-${index}`}>\n              {char}\n            </tspan>\n          ))}\n        </text>\n      </svg>\n    </span>\n  );\n};\n\nexport default StrokeText;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0"
	]
}