{
  "name": "scroll-text",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "ui/scroll-text.tsx",
      "content": "import {\n  type MotionValue,\n  motion,\n  useScroll,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\ntype ScrollEffect =\n  | \"fadeIn\"\n  | \"fadeUp\"\n  | \"fadeDown\"\n  | \"parallax\"\n  | \"scale\"\n  | \"scaleUp\"\n  | \"scaleDown\"\n  | \"rotate\"\n  | \"blur\"\n  | \"slideLeft\"\n  | \"slideRight\"\n  | \"skew\"\n  | \"flip\"\n  | \"reveal\";\n\ninterface ScrollTextProps {\n  children: React.ReactNode;\n  effect?: ScrollEffect;\n  className?: string;\n  offset?: [string, string];\n  speed?: number;\n  spring?: boolean;\n  springConfig?: { stiffness?: number; damping?: number; mass?: number };\n  threshold?: [number, number];\n}\n\ninterface ParallaxTextProps {\n  children: React.ReactNode;\n  className?: string;\n  speed?: number;\n  direction?: \"up\" | \"down\" | \"left\" | \"right\";\n  spring?: boolean;\n}\n\ninterface ScrollRevealProps {\n  children: React.ReactNode;\n  className?: string;\n  direction?: \"up\" | \"down\" | \"left\" | \"right\";\n  delay?: number;\n  duration?: number;\n  distance?: number;\n  once?: boolean;\n}\n\ninterface ScrollFadeProps {\n  children: React.ReactNode;\n  className?: string;\n  fadeIn?: boolean;\n  fadeOut?: boolean;\n  threshold?: [number, number];\n}\n\ninterface ScrollScaleProps {\n  children: React.ReactNode;\n  className?: string;\n  from?: number;\n  to?: number;\n  threshold?: [number, number];\n}\n\ninterface HorizontalScrollTextProps {\n  children: React.ReactNode;\n  className?: string;\n  speed?: number;\n  direction?: \"left\" | \"right\";\n  repeat?: number;\n}\n\ninterface ScrollProgressTextProps {\n  text: string;\n  className?: string;\n  charClassName?: string;\n  stagger?: number;\n}\n\ninterface StickyScrollTextProps {\n  children: React.ReactNode;\n  className?: string;\n  height?: string;\n  effect?: \"fade\" | \"scale\" | \"blur\" | \"color\";\n}\n\n// ============================================================================\n// SCROLL TEXT COMPONENT\n// ============================================================================\n\nconst ScrollText = React.forwardRef<HTMLDivElement, ScrollTextProps>(\n  (\n    {\n      children,\n      effect = \"fadeIn\",\n      className,\n      offset = [\"start end\", \"end start\"],\n      speed = 1,\n      spring = false,\n      springConfig,\n      threshold = [0, 1],\n    },\n    forwardedRef,\n  ) => {\n    const internalRef = React.useRef<HTMLDivElement>(null);\n    const ref =\n      (forwardedRef as React.RefObject<HTMLDivElement>) || internalRef;\n\n    const { scrollYProgress } = useScroll({\n      target: ref,\n      offset: offset as [\"start end\", \"end start\"],\n    });\n\n    const [start, end] = threshold;\n    const springOpts = {\n      stiffness: springConfig?.stiffness ?? 100,\n      damping: springConfig?.damping ?? 30,\n      mass: springConfig?.mass ?? 1,\n    };\n\n    // Create transforms\n    const rawOpacity = useTransform(scrollYProgress, [start, end], [0, 1]);\n    const rawY = useTransform(scrollYProgress, [start, end], [50 * speed, 0]);\n    const rawYDown = useTransform(\n      scrollYProgress,\n      [start, end],\n      [-50 * speed, 0],\n    );\n    const rawYParallax = useTransform(\n      scrollYProgress,\n      [0, 1],\n      [100 * speed, -100 * speed],\n    );\n    const rawScale = useTransform(scrollYProgress, [start, end], [0.5, 1]);\n    const rawScaleUp = useTransform(\n      scrollYProgress,\n      [0, 0.5, 1],\n      [0.8, 1, 1.2],\n    );\n    const rawScaleDown = useTransform(\n      scrollYProgress,\n      [0, 0.5, 1],\n      [1.2, 1, 0.8],\n    );\n    const rawRotate = useTransform(scrollYProgress, [0, 1], [0, 360 * speed]);\n    const rawBlurOpacity = useTransform(\n      scrollYProgress,\n      [start, end],\n      [0.5, 1],\n    );\n    const rawX = useTransform(scrollYProgress, [start, end], [200 * speed, 0]);\n    const rawXRight = useTransform(\n      scrollYProgress,\n      [start, end],\n      [-200 * speed, 0],\n    );\n    const rawSkew = useTransform(\n      scrollYProgress,\n      [start, end],\n      [20 * speed, 0],\n    );\n    const rawRotateX = useTransform(scrollYProgress, [start, end], [90, 0]);\n\n    // Apply spring if needed\n    const opacitySpring = useSpring(rawOpacity, springOpts);\n    const ySpring = useSpring(rawY, springOpts);\n    const yDownSpring = useSpring(rawYDown, springOpts);\n    const yParallaxSpring = useSpring(rawYParallax, springOpts);\n    const scaleSpring = useSpring(rawScale, springOpts);\n    const scaleUpSpring = useSpring(rawScaleUp, springOpts);\n    const scaleDownSpring = useSpring(rawScaleDown, springOpts);\n    const rotateSpring = useSpring(rawRotate, springOpts);\n    const blurOpacitySpring = useSpring(rawBlurOpacity, springOpts);\n    const rotateXSpring = useSpring(rawRotateX, springOpts);\n\n    const opacity = spring ? opacitySpring : rawOpacity;\n    const y = spring ? ySpring : rawY;\n    const yDown = spring ? yDownSpring : rawYDown;\n    const yParallax = spring ? yParallaxSpring : rawYParallax;\n    const scale = spring ? scaleSpring : rawScale;\n    const scaleUp = spring ? scaleUpSpring : rawScaleUp;\n    const scaleDown = spring ? scaleDownSpring : rawScaleDown;\n    const rotate = spring ? rotateSpring : rawRotate;\n    const blurOpacity = spring ? blurOpacitySpring : rawBlurOpacity;\n    const rotateX = spring ? rotateXSpring : rawRotateX;\n    const xSpring = useSpring(rawX, springOpts);\n    const xRightSpring = useSpring(rawXRight, springOpts);\n    const skewSpring = useSpring(rawSkew, springOpts);\n\n    const x = spring ? xSpring : rawX;\n    const xRight = spring ? xRightSpring : rawXRight;\n    const skew = spring ? skewSpring : rawSkew;\n\n    // Non-springable transforms\n    const blur = useTransform(scrollYProgress, [start, end], [10 * speed, 0]);\n    const clipPath = useTransform(scrollYProgress, [start, end], [100, 0]);\n\n    const effectStyles: Record<\n      ScrollEffect,\n      Record<string, MotionValue<number>>\n    > = {\n      fadeIn: { opacity },\n      fadeUp: { opacity, y },\n      fadeDown: { opacity, y: yDown },\n      parallax: { y: yParallax },\n      scale: { scale, opacity },\n      scaleUp: { scale: scaleUp },\n      scaleDown: { scale: scaleDown },\n      rotate: { rotate },\n      blur: { opacity: blurOpacity },\n      slideLeft: { x, opacity },\n      slideRight: { x: xRight, opacity },\n      skew: { skewX: skew, opacity },\n      flip: { rotateX, opacity },\n      reveal: {},\n    };\n\n    const filterBlur = useTransform(blur, (v) => `blur(${v}px)`);\n    const clipPathValue = useTransform(clipPath, (v) => `inset(0 ${v}% 0 0)`);\n\n    return (\n      <motion.div\n        ref={ref}\n        className={cn(\"will-change-transform\", className)}\n        style={{\n          ...effectStyles[effect],\n          ...(effect === \"blur\" && { filter: filterBlur }),\n          ...(effect === \"reveal\" && { clipPath: clipPathValue }),\n          ...(effect === \"flip\" && { perspective: 1000 }),\n        }}\n      >\n        {children}\n      </motion.div>\n    );\n  },\n);\n\nScrollText.displayName = \"ScrollText\";\n\n// ============================================================================\n// PARALLAX TEXT COMPONENT\n// ============================================================================\n\nconst ParallaxText = React.forwardRef<HTMLDivElement, ParallaxTextProps>(\n  (\n    { children, className, speed = 1, direction = \"up\", spring = true },\n    ref,\n  ) => {\n    const containerRef = React.useRef<HTMLDivElement>(null);\n    const { scrollYProgress } = useScroll({\n      target: containerRef,\n      offset: [\"start end\", \"end start\"],\n    });\n\n    const distance = 100 * speed;\n    const springOpts = spring\n      ? { stiffness: 100, damping: 30 }\n      : { stiffness: 1000, damping: 1000 };\n\n    const yUp = useSpring(\n      useTransform(scrollYProgress, [0, 1], [distance, -distance]),\n      springOpts,\n    );\n    const yDown = useSpring(\n      useTransform(scrollYProgress, [0, 1], [-distance, distance]),\n      springOpts,\n    );\n    const xLeft = useSpring(\n      useTransform(scrollYProgress, [0, 1], [distance, -distance]),\n      springOpts,\n    );\n    const xRight = useSpring(\n      useTransform(scrollYProgress, [0, 1], [-distance, distance]),\n      springOpts,\n    );\n\n    const isHorizontal = direction === \"left\" || direction === \"right\";\n    const transform = {\n      up: yUp,\n      down: yDown,\n      left: xLeft,\n      right: xRight,\n    }[direction];\n\n    return (\n      <div ref={containerRef} className={cn(\"overflow-hidden\", className)}>\n        <motion.div\n          ref={ref}\n          style={{ [isHorizontal ? \"x\" : \"y\"]: transform }}\n          className=\"will-change-transform\"\n        >\n          {children}\n        </motion.div>\n      </div>\n    );\n  },\n);\n\nParallaxText.displayName = \"ParallaxText\";\n\n// ============================================================================\n// SCROLL REVEAL COMPONENT\n// ============================================================================\n\nconst ScrollReveal = React.forwardRef<HTMLDivElement, ScrollRevealProps>(\n  (\n    {\n      children,\n      className,\n      direction = \"up\",\n      delay = 0,\n      duration = 0.6,\n      distance = 60,\n      once = true,\n    },\n    ref,\n  ) => {\n    const containerRef = React.useRef<HTMLDivElement>(null);\n    const [isInView, setIsInView] = React.useState(false);\n\n    React.useEffect(() => {\n      const observer = new IntersectionObserver(\n        ([entry]) => {\n          if (entry?.isIntersecting) {\n            setIsInView(true);\n            if (once && containerRef.current) {\n              observer.unobserve(containerRef.current);\n            }\n          } else if (!once) {\n            setIsInView(false);\n          }\n        },\n        { threshold: 0.1 },\n      );\n\n      if (containerRef.current) {\n        observer.observe(containerRef.current);\n      }\n\n      return () => observer.disconnect();\n    }, [once]);\n\n    const getInitialPosition = () => {\n      switch (direction) {\n        case \"up\":\n          return { y: distance, x: 0 };\n        case \"down\":\n          return { y: -distance, x: 0 };\n        case \"left\":\n          return { x: distance, y: 0 };\n        case \"right\":\n          return { x: -distance, y: 0 };\n      }\n    };\n\n    const initial = getInitialPosition();\n\n    return (\n      <div ref={containerRef} className={className}>\n        <motion.div\n          ref={ref}\n          initial={{ opacity: 0, ...initial }}\n          animate={\n            isInView ? { opacity: 1, x: 0, y: 0 } : { opacity: 0, ...initial }\n          }\n          transition={{ duration, delay, ease: [0.25, 0.46, 0.45, 0.94] }}\n        >\n          {children}\n        </motion.div>\n      </div>\n    );\n  },\n);\n\nScrollReveal.displayName = \"ScrollReveal\";\n\n// ============================================================================\n// SCROLL FADE COMPONENT\n// ============================================================================\n\nconst ScrollFade = React.forwardRef<HTMLDivElement, ScrollFadeProps>(\n  (\n    {\n      children,\n      className,\n      fadeIn = true,\n      fadeOut = true,\n      threshold = [0.2, 0.8],\n    },\n    ref,\n  ) => {\n    const containerRef = React.useRef<HTMLDivElement>(null);\n    const { scrollYProgress } = useScroll({\n      target: containerRef,\n      offset: [\"start end\", \"end start\"],\n    });\n\n    const opacity = useTransform(scrollYProgress, (value) => {\n      if (fadeIn && fadeOut) {\n        if (value < threshold[0]) return value / threshold[0];\n        if (value > threshold[1])\n          return 1 - (value - threshold[1]) / (1 - threshold[1]);\n        return 1;\n      }\n      if (fadeIn) return Math.min(value / threshold[0], 1);\n      if (fadeOut)\n        return value > threshold[1]\n          ? 1 - (value - threshold[1]) / (1 - threshold[1])\n          : 1;\n      return 1;\n    });\n\n    return (\n      <motion.div ref={containerRef} className={className} style={{ opacity }}>\n        <div ref={ref}>{children}</div>\n      </motion.div>\n    );\n  },\n);\n\nScrollFade.displayName = \"ScrollFade\";\n\n// ============================================================================\n// SCROLL SCALE COMPONENT\n// ============================================================================\n\nconst ScrollScale = React.forwardRef<HTMLDivElement, ScrollScaleProps>(\n  ({ children, className, from = 0.8, to = 1, threshold = [0, 0.5] }, ref) => {\n    const containerRef = React.useRef<HTMLDivElement>(null);\n    const { scrollYProgress } = useScroll({\n      target: containerRef,\n      offset: [\"start end\", \"end start\"],\n    });\n\n    const scale = useSpring(\n      useTransform(scrollYProgress, threshold, [from, to]),\n      { stiffness: 100, damping: 30 },\n    );\n    const opacity = useTransform(scrollYProgress, threshold, [0, 1]);\n\n    return (\n      <motion.div\n        ref={containerRef}\n        className={cn(\"will-change-transform\", className)}\n        style={{ scale, opacity }}\n      >\n        <div ref={ref}>{children}</div>\n      </motion.div>\n    );\n  },\n);\n\nScrollScale.displayName = \"ScrollScale\";\n\n// ============================================================================\n// HORIZONTAL SCROLL TEXT COMPONENT\n// ============================================================================\n\nconst HorizontalScrollText = React.forwardRef<\n  HTMLDivElement,\n  HorizontalScrollTextProps\n>(({ children, className, speed = 1, direction = \"left\", repeat = 3 }, ref) => {\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const { scrollYProgress } = useScroll({\n    target: containerRef,\n    offset: [\"start end\", \"end start\"],\n  });\n\n  const x = useTransform(\n    scrollYProgress,\n    [0, 1],\n    direction === \"left\"\n      ? [\"0%\", `-${50 * speed}%`]\n      : [`-${50 * speed}%`, \"0%\"],\n  );\n\n  return (\n    <div ref={containerRef} className={cn(\"overflow-hidden\", className)}>\n      <motion.div\n        ref={ref}\n        className=\"flex whitespace-nowrap will-change-transform\"\n        style={{ x }}\n      >\n        {Array.from({ length: repeat }).map((_, i) => (\n          <span key={i} className=\"flex-shrink-0 px-4\">\n            {children}\n          </span>\n        ))}\n      </motion.div>\n    </div>\n  );\n});\n\nHorizontalScrollText.displayName = \"HorizontalScrollText\";\n\n// ============================================================================\n// SCROLL PROGRESS TEXT COMPONENT\n// ============================================================================\n\ninterface ScrollProgressCharProps {\n  char: string;\n  progress: MotionValue<number>;\n  range: [number, number];\n  className?: string;\n}\n\nconst ScrollProgressChar: React.FC<ScrollProgressCharProps> = ({\n  char,\n  progress,\n  range,\n  className,\n}) => {\n  const opacity = useTransform(progress, range, [0.2, 1]);\n  const color = useTransform(progress, range, [\n    \"hsl(var(--muted-foreground))\",\n    \"hsl(var(--foreground))\",\n  ]);\n\n  return (\n    <motion.span\n      style={{ opacity, color }}\n      className={cn(\"transition-colors\", className)}\n    >\n      {char === \" \" ? \"\\u00A0\" : char}\n    </motion.span>\n  );\n};\n\nconst ScrollProgressText = React.forwardRef<\n  HTMLDivElement,\n  ScrollProgressTextProps\n>(({ text, className, charClassName }, ref) => {\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const { scrollYProgress } = useScroll({\n    target: containerRef,\n    offset: [\"start 0.9\", \"start 0.25\"],\n  });\n\n  const characters = text.split(\"\");\n  const step = 1 / characters.length;\n\n  return (\n    <div ref={containerRef} className={className}>\n      <span ref={ref} className=\"flex flex-wrap\">\n        {characters.map((char, index) => {\n          const start = index * step;\n          const end = start + step;\n          return (\n            <ScrollProgressChar\n              key={index}\n              char={char}\n              progress={scrollYProgress}\n              range={[start, end]}\n              className={charClassName}\n            />\n          );\n        })}\n      </span>\n    </div>\n  );\n});\n\nScrollProgressText.displayName = \"ScrollProgressText\";\n\n// ============================================================================\n// STICKY SCROLL TEXT COMPONENT\n// ============================================================================\n\nconst StickyScrollText = React.forwardRef<\n  HTMLDivElement,\n  StickyScrollTextProps\n>(({ children, className, height = \"200vh\", effect = \"fade\" }, ref) => {\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const { scrollYProgress } = useScroll({\n    target: containerRef,\n    offset: [\"start start\", \"end start\"],\n  });\n\n  const opacityFade = useTransform(\n    scrollYProgress,\n    [0, 0.3, 0.7, 1],\n    [0, 1, 1, 0],\n  );\n  const scaleValue = useTransform(\n    scrollYProgress,\n    [0, 0.3, 0.7, 1],\n    [0.5, 1, 1, 0.5],\n  );\n  const blurValue = useTransform(\n    scrollYProgress,\n    [0, 0.3, 0.7, 1],\n    [10, 0, 0, 10],\n  );\n  const filterBlur = useTransform(blurValue, (v) => `blur(${v}px)`);\n  const color = useTransform(\n    scrollYProgress,\n    [0, 0.5, 1],\n    [\n      \"hsl(var(--muted-foreground))\",\n      \"hsl(var(--primary))\",\n      \"hsl(var(--muted-foreground))\",\n    ],\n  );\n\n  const effectStyles = {\n    fade: { opacity: opacityFade },\n    scale: { scale: scaleValue, opacity: opacityFade },\n    blur: { filter: filterBlur, opacity: opacityFade },\n    color: { opacity: opacityFade, color },\n  };\n\n  return (\n    <div ref={containerRef} className=\"relative\" style={{ height }}>\n      <motion.div\n        ref={ref}\n        className={cn(\n          \"sticky top-1/2 -translate-y-1/2 will-change-transform\",\n          className,\n        )}\n        style={effectStyles[effect]}\n      >\n        {children}\n      </motion.div>\n    </div>\n  );\n});\n\nStickyScrollText.displayName = \"StickyScrollText\";\n\n// ============================================================================\n// EXPORTS\n// ============================================================================\n\nexport {\n  HorizontalScrollText,\n  ParallaxText,\n  ScrollFade,\n  ScrollProgressText,\n  ScrollReveal,\n  ScrollScale,\n  ScrollText,\n  StickyScrollText,\n};\n\nexport type {\n  HorizontalScrollTextProps,\n  ParallaxTextProps,\n  ScrollEffect,\n  ScrollFadeProps,\n  ScrollProgressTextProps,\n  ScrollRevealProps,\n  ScrollScaleProps,\n  ScrollTextProps,\n  StickyScrollTextProps,\n};\n",
      "type": "registry:ui",
      "target": ""
    }
  ],
  "type": "registry:ui"
}