{ "name": "text-circle-scroll", "type": "registry:ui", "registryDependencies": [], "dependencies": [ "motion" ], "devDependencies": [], "tailwind": {}, "cssVars": { "light": {}, "dark": {} }, "files": [ { "path": "text-circle-scroll.tsx", "content": "'use client';\n\nimport * as React from 'react';\nimport { motion, useScroll, useTransform, useSpring } from 'motion/react';\nimport { cn } from '@/lib/utils';\n\nexport type TextCircleItem = string | React.ReactNode;\n\nexport type TextCircleScrollProps = {\n /** Items placed on the ring in order (clockwise by default). */\n items: TextCircleItem[];\n\n /** Radius in px (distance from center to baseline of items). */\n radius?: number;\n\n /** Empty hole diameter in px (purely visual helper class on container). */\n innerGap?: number;\n\n /** Angle offset in degrees for the first item. */\n startAngle?: number;\n\n /** Clockwise (true) or counter-clockwise (false) item order. */\n clockwise?: boolean;\n\n /** If true, connects rotation to page scroll progress. */\n rotateOnScroll?: boolean;\n\n /**\n * How many degrees the ring rotates from top->bottom of the page when\n * rotateOnScroll = true. 360 = one full turn.\n */\n scrollDegrees?: number;\n\n /** Constant rotation (deg/s). Can be negative for reverse. */\n autoSpinDegPerSec?: number;\n\n /** Springiness for scroll rotation. Set 0 to disable spring. */\n springStiffness?: number;\n\n /** Container className */\n className?: string;\n\n /** Ring element className */\n ringClassName?: string;\n\n /** Per-item className */\n itemClassName?: string;\n\n /** Typography class for items (e.g. 'text-lg font-serif') */\n textClassName?: string;\n\n /** If provided, constrain the component height (useful in docs/examples). */\n height?: number | string;\n};\n\n/**\n * TextCircleScroll\n * - Places any text/nodes around a circle\n * - Rotates with scroll (and/or auto-spin)\n * - Purely presentational; does not manage layout outside itself\n */\nexport default function TextCircleScroll({\n items,\n radius = 110,\n innerGap = 90,\n startAngle = -90,\n clockwise = true,\n rotateOnScroll = true,\n scrollDegrees = 360,\n autoSpinDegPerSec = 0,\n springStiffness = 120,\n className,\n ringClassName,\n itemClassName,\n textClassName = 'text-base font-serif text-zinc-800 dark:text-zinc-200',\n height = 320,\n}: TextCircleScrollProps) {\n const ref = React.useRef(null);\n\n // --- Scroll-driven rotation\n const { scrollYProgress } = useScroll({\n // when the component is within the viewport\n target: ref,\n offset: ['start end', 'end start'],\n });\n\n const rawRotate = useTransform(scrollYProgress, [0, 1], [0, scrollDegrees]);\n const rotateSpring = useSpring(rawRotate, {\n stiffness: springStiffness || 120,\n damping: 18,\n mass: 0.6,\n });\n\n // --- Auto-spin via requestAnimationFrame\n const [autoAngle, setAutoAngle] = React.useState(0);\n React.useEffect(() => {\n if (!autoSpinDegPerSec) return;\n let raf = 0;\n let last = performance.now();\n const tick = (t: number) => {\n const dt = (t - last) / 1000;\n last = t;\n setAutoAngle((a) => a + autoSpinDegPerSec * dt);\n raf = requestAnimationFrame(tick);\n };\n raf = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(raf);\n }, [autoSpinDegPerSec]);\n\n // Combine scroll rotation + auto rotation.\n const [combined, setCombined] = React.useState(0);\n React.useEffect(() => {\n const unsub = rotateSpring.on('change', (v) => setCombined(v));\n return () => unsub();\n }, [rotateSpring]);\n\n const totalRotation = (rotateOnScroll ? combined : 0) + autoAngle;\n\n const n = items.length;\n const step = 360 / Math.max(1, n);\n const dir = clockwise ? 1 : -1;\n\n return (\n \n {/* Visual inner hole helper */}\n \n {/* Ring */}\n \n {items.map((item, i) => {\n const angle = startAngle + dir * i * step;\n // Place at angle: rotate container then translate then un-rotate\n const style: React.CSSProperties = {\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: `rotate(${angle}deg) translate(${radius}px) rotate(${clockwise ? 90 : -90}deg)`,\n transformOrigin: '0 0',\n whiteSpace: 'nowrap',\n };\n return (\n \n \n {typeof item === 'string' ? item : item}\n \n \n );\n })}\n \n \n );\n}\n", "type": "registry:ui" } ] }