{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "CurvedLoop-TS-CSS",
	"title": "CurvedLoop",
	"description": "Flowing looping text path along a customizable curve with drag interaction.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "CurvedLoop.css",
			"target": "@components/CurvedLoop.css",
			"content": ".curved-loop-jacket {\n  min-height: 100vh;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n}\n\n.curved-loop-svg {\n  user-select: none;\n  width: 100%;\n  aspect-ratio: 100 / 12;\n  overflow: visible;\n  display: block;\n  font-size: 6rem;\n  fill: #ffffff;\n  user-select: none;\n  -moz-user-select: none;\n  -webkit-user-select: none;\n  font-weight: 700;\n  text-transform: uppercase;\n  line-height: 1;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "CurvedLoop.tsx",
			"content": "import { useRef, useEffect, useState, useMemo, useId, type FC, type PointerEvent } from 'react';\nimport './CurvedLoop.css';\n\ninterface CurvedLoopProps {\n  marqueeText?: string;\n  speed?: number;\n  className?: string;\n  curveAmount?: number;\n  direction?: 'left' | 'right';\n  interactive?: boolean;\n}\n\nconst CurvedLoop: FC<CurvedLoopProps> = ({\n  marqueeText = '',\n  speed = 2,\n  className,\n  curveAmount = 400,\n  direction = 'left',\n  interactive = true\n}) => {\n  const text = useMemo(() => {\n    const hasTrailing = /\\s|\\u00A0$/.test(marqueeText);\n    return (hasTrailing ? marqueeText.replace(/\\s+$/, '') : marqueeText) + '\\u00A0';\n  }, [marqueeText]);\n\n  const measureRef = useRef<SVGTextElement | null>(null);\n  const textPathRef = useRef<SVGTextPathElement | null>(null);\n  const pathRef = useRef<SVGPathElement | null>(null);\n  const [spacing, setSpacing] = useState(0);\n  const [offset, setOffset] = useState(0);\n  const uid = useId();\n  const pathId = `curve-${uid}`;\n  const pathD = `M-100,40 Q500,${40 + curveAmount} 1540,40`;\n\n  const dragRef = useRef(false);\n  const lastXRef = useRef(0);\n  const dirRef = useRef<'left' | 'right'>(direction);\n  const velRef = useRef(0);\n\n  const textLength = spacing;\n  const totalText = textLength\n    ? Array(Math.ceil(1800 / textLength) + 2)\n        .fill(text)\n        .join('')\n    : text;\n  const ready = spacing > 0;\n\n  useEffect(() => {\n    if (measureRef.current) setSpacing(measureRef.current.getComputedTextLength());\n  }, [text, className]);\n\n  useEffect(() => {\n    if (!spacing) return;\n    if (textPathRef.current) {\n      const initial = -spacing;\n      textPathRef.current.setAttribute('startOffset', initial + 'px');\n      setOffset(initial);\n    }\n  }, [spacing]);\n\n  useEffect(() => {\n    if (!spacing || !ready) return;\n    let frame = 0;\n    const step = () => {\n      if (!dragRef.current && textPathRef.current) {\n        const delta = dirRef.current === 'right' ? speed : -speed;\n        const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n        let newOffset = currentOffset + delta;\n        const wrapPoint = spacing;\n        if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n        if (newOffset > 0) newOffset -= wrapPoint;\n        textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n        setOffset(newOffset);\n      }\n      frame = requestAnimationFrame(step);\n    };\n    frame = requestAnimationFrame(step);\n    return () => cancelAnimationFrame(frame);\n  }, [spacing, speed, ready]);\n\n  const onPointerDown = (e: PointerEvent) => {\n    if (!interactive) return;\n    dragRef.current = true;\n    lastXRef.current = e.clientX;\n    velRef.current = 0;\n    (e.target as HTMLElement).setPointerCapture(e.pointerId);\n  };\n\n  const onPointerMove = (e: PointerEvent) => {\n    if (!interactive || !dragRef.current || !textPathRef.current) return;\n    const dx = e.clientX - lastXRef.current;\n    lastXRef.current = e.clientX;\n    velRef.current = dx;\n    const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');\n    let newOffset = currentOffset + dx;\n    const wrapPoint = spacing;\n    if (newOffset <= -wrapPoint) newOffset += wrapPoint;\n    if (newOffset > 0) newOffset -= wrapPoint;\n    textPathRef.current.setAttribute('startOffset', newOffset + 'px');\n    setOffset(newOffset);\n  };\n\n  const endDrag = () => {\n    if (!interactive) return;\n    dragRef.current = false;\n    dirRef.current = velRef.current > 0 ? 'right' : 'left';\n  };\n\n  const cursorStyle = interactive ? (dragRef.current ? 'grabbing' : 'grab') : 'auto';\n\n  return (\n    <div\n      className=\"curved-loop-jacket\"\n      style={{ visibility: ready ? 'visible' : 'hidden', cursor: cursorStyle }}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={endDrag}\n      onPointerLeave={endDrag}\n    >\n      <svg className=\"curved-loop-svg\" viewBox=\"0 0 1440 120\">\n        <text ref={measureRef} xmlSpace=\"preserve\" style={{ visibility: 'hidden', opacity: 0, pointerEvents: 'none' }}>\n          {text}\n        </text>\n        <defs>\n          <path ref={pathRef} id={pathId} d={pathD} fill=\"none\" stroke=\"transparent\" />\n        </defs>\n        {ready && (\n          <text fontWeight=\"bold\" xmlSpace=\"preserve\" className={className}>\n            <textPath ref={textPathRef} href={`#${pathId}`} startOffset={offset + 'px'} xmlSpace=\"preserve\">\n              {totalText}\n            </textPath>\n          </text>\n        )}\n      </svg>\n    </div>\n  );\n};\n\nexport default CurvedLoop;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}