{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-text",
  "type": "registry:component",
  "title": "Scroll Text",
  "description": "Scroll-snapping word list that highlights the centered item via IntersectionObserver (KokonutUI, MIT), bridged to contract tokens.",
  "dependencies": [
    "framer-motion"
  ],
  "registryDependencies": [
    "https://design.subconscious.ai/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/components/scroll-text.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Scroll Text\n * @version: 1.0.0\n * @date: 2025-06-26\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { motion, type Variants } from \"framer-motion\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/registry/lib/utils\";\n\ninterface ScrollTextProps {\n  texts?: string[];\n  className?: string;\n  autoCycle?: boolean;\n  interval?: number;\n}\n\nexport default function ScrollText({\n  texts = [\n    \"TailwindCSS\",\n    \"Kokonut UI\",\n    \"shadcn/ui\",\n    \"Next.js\",\n    \"Vercel\",\n    \"Motion\",\n    \"React\",\n    \"Resend\",\n    \"TypeScript\",\n    \"Fumadocs\",\n    \"Supabase\",\n    \"Vercel\",\n  ],\n  className,\n  autoCycle = true,\n  interval = 2500,\n}: ScrollTextProps) {\n  const [activeIndex, setActiveIndex] = useState(0);\n  const [isPaused, setIsPaused] = useState(false);\n  const observerRef = useRef<IntersectionObserver | null>(null);\n  const itemsRef = useRef<(HTMLDivElement | null)[]>([]);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const resumeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  // Scroll to top on mount\n  useEffect(() => {\n    if (containerRef.current) {\n      containerRef.current.scrollTop = 0;\n    }\n  }, []);\n\n  // Auto-cycle: advance activeIndex modulo texts.length and scroll the\n  // intended item into the observer's center band so the IntersectionObserver\n  // doesn't clobber the auto-advance back to whatever item is in view.\n  useEffect(() => {\n    if (!autoCycle || interval <= 0 || isPaused || texts.length < 2) return;\n    // Respect prefers-reduced-motion: skip auto-cycle entirely.\n    if (\n      typeof window !== \"undefined\" &&\n      window.matchMedia &&\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    ) {\n      return;\n    }\n    const id = setInterval(() => {\n      setActiveIndex((i) => {\n        const next = (i + 1) % texts.length;\n        // Scroll ONLY the inner overflow-y-auto container — never bubble\n        // out to the document. scrollIntoView() walks every scrollable\n        // ancestor and yanks the page; manually setting container.scrollTop\n        // keeps the page anchored where the reader has it.\n        const container = containerRef.current;\n        const target = itemsRef.current[next];\n        if (container && target) {\n          container.scrollTo({\n            top:\n              target.offsetTop -\n              container.clientHeight / 2 +\n              target.offsetHeight / 2,\n            behavior: \"smooth\",\n          });\n        }\n        return next;\n      });\n    }, interval);\n    return () => clearInterval(id);\n  }, [autoCycle, interval, isPaused, texts.length]);\n\n  // Pause when the user takes over; resume after a short idle.\n  const pauseCycle = () => {\n    setIsPaused(true);\n    if (resumeTimerRef.current) {\n      clearTimeout(resumeTimerRef.current);\n      resumeTimerRef.current = null;\n    }\n  };\n\n  const scheduleResume = () => {\n    if (resumeTimerRef.current) clearTimeout(resumeTimerRef.current);\n    resumeTimerRef.current = setTimeout(() => {\n      setIsPaused(false);\n      resumeTimerRef.current = null;\n    }, 1500);\n  };\n\n  useEffect(() => {\n    return () => {\n      if (resumeTimerRef.current) clearTimeout(resumeTimerRef.current);\n    };\n  }, []);\n\n  const handleIntersection = (entries: IntersectionObserverEntry[]) => {\n    entries.forEach((entry) => {\n      if (entry.isIntersecting) {\n        const index = itemsRef.current.findIndex(\n          (item) => item === entry.target\n        );\n        setActiveIndex(index);\n      }\n    });\n  };\n\n  // Setup intersection observer\n  const setupObserver = (element: HTMLDivElement | null, index: number) => {\n    if (element && !itemsRef.current[index]) {\n      itemsRef.current[index] = element;\n\n      if (!observerRef.current) {\n        observerRef.current = new IntersectionObserver(handleIntersection, {\n          threshold: 0.7,\n          root: containerRef.current,\n          rootMargin: \"-45% 0px -45% 0px\",\n        });\n      }\n\n      observerRef.current.observe(element);\n    }\n  };\n\n  // Animation variants for the reveal effect\n  const containerVariants: Variants = {\n    hidden: { opacity: 0 },\n    visible: {\n      opacity: 1,\n      transition: {\n        staggerChildren: 0.1,\n      },\n    },\n  };\n\n  const itemVariants: Variants = {\n    hidden: (index: number) => ({\n      opacity: 0,\n      x: index % 2 === 0 ? -100 : 100,\n      rotate: index % 2 === 0 ? -10 : 10,\n    }),\n    visible: {\n      opacity: 1,\n      x: 0,\n      rotate: 0,\n      transition: {\n        type: \"spring\",\n        stiffness: 100,\n        damping: 15,\n        duration: 0.5,\n      },\n    },\n  };\n\n  return (\n    <div className={cn(\"w-full\", className)}>\n      <div\n        className={cn(\n          \"scrollbar-none h-[300px] overflow-y-auto\",\n          \"relative flex flex-col items-center\",\n          \"[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n        )}\n        ref={containerRef}\n        onWheel={pauseCycle}\n        onTouchStart={pauseCycle}\n        onMouseLeave={scheduleResume}\n        onTouchEnd={scheduleResume}\n      >\n        <div className=\"h-[150px]\" />\n        <motion.div\n          animate=\"visible\"\n          className=\"flex w-full flex-col items-center\"\n          initial=\"hidden\"\n          variants={containerVariants}\n        >\n          {texts.map((text, index) => (\n            <motion.div\n              className={cn(\n                \"whitespace-nowrap px-4 py-8 font-bold text-5xl\",\n                \"transition-colors duration-300\"\n              )}\n              style={{\n                color:\n                  activeIndex === index\n                    ? \"var(--color)\"\n                    : \"color-mix(in srgb, var(--color) 30%, transparent)\",\n              }}\n              custom={index}\n              initial=\"hidden\"\n              key={text}\n              ref={(el) => setupObserver(el, index)}\n              variants={itemVariants}\n              viewport={{\n                once: false,\n                margin: \"-20% 0px -20% 0px\",\n              }}\n              whileInView=\"visible\"\n            >\n              {text}\n            </motion.div>\n          ))}\n        </motion.div>\n        <div className=\"h-[150px]\" />\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}
