{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cursor-highlight",
  "type": "registry:ui",
  "title": "Cursor Highlight",
  "description": "Text is revealed on scroll with pointer, gradient, and border effects.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["motion", "clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/cursor-highlight.tsx",
      "content": "'use client';\n\nimport React, { useRef, useEffect, useState } from 'react';\nimport { motion, useInView } from 'motion/react';\nimport { cn } from '@/lib/utils';\n\ninterface CursorHighlightProps {\n  text?: string;\n  children?: React.ReactNode;\n  className?: string;\n  gradient?: string;\n  direction?: 'left' | 'right' | 'top' | 'bottom';\n  animate?: boolean;\n  duration?: number;\n  showPointer?: boolean;\n  pointerDuration?: number;\n  pointerClassName?: string;\n  containerClassName?: string;\n  rectangle?: boolean | string;\n}\n\nconst Pointer = (props: React.SVGProps<SVGSVGElement>) => (\n  <svg\n    stroke='currentColor'\n    fill='currentColor'\n    strokeWidth='1'\n    strokeLinecap='round'\n    strokeLinejoin='round'\n    viewBox='0 0 16 16'\n    height='1em'\n    width='1em'\n    xmlns='http://www.w3.org/2000/svg'\n    {...props}\n  >\n    <path d='M14.082 2.182a.5.5 0 0 1 .103.557L8.528 15.467a.5.5 0 0 1-.917-.007L5.57 10.694.803 8.652a.5.5 0 0 1-.006-.916l12.728-5.657a.5.5 0 0 1 .556.103z'></path>\n  </svg>\n);\n\ninterface PointerAnim {\n  initial: { x: number; y: number; rotate: number };\n  animate: { x: number; y: number; rotate: number };\n}\n\nexport function CursorHighlight({\n  text,\n  children,\n  className,\n  gradient = 'from-purple-800 via-purple-600 to-purple-800',\n  direction = 'left',\n  animate = false,\n  duration = 2,\n  showPointer = false,\n  pointerDuration = 1,\n  pointerClassName,\n  containerClassName,\n  rectangle = false,\n}: CursorHighlightProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const isInView = useInView(containerRef, { amount: 0.3, once: false });\n  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n  const [resetKey, setResetKey] = useState(0);\n\n  useEffect(() => {\n    const element = containerRef.current;\n    if (element) {\n      const { width, height } = element.getBoundingClientRect();\n      setDimensions({ width, height });\n    }\n\n    const resizeObserver = new ResizeObserver((entries) => {\n      for (const entry of entries) {\n        const { width, height } = entry.contentRect;\n        setDimensions({ width, height });\n      }\n    });\n\n    if (element) resizeObserver.observe(element);\n    return () => {\n      if (element) resizeObserver.unobserve(element);\n    };\n  }, [text, children]);\n\n  useEffect(() => {\n    if (!isInView) {\n      const timeout = setTimeout(() => setResetKey((k) => k + 1), 200);\n      return () => clearTimeout(timeout);\n    }\n  }, [isInView]);\n\n  const content = text || children;\n  const hasCustomTextColor =\n    className &&\n    className.includes('text-') &&\n    (className.includes('dark:text-') || !className.includes('dark:'));\n\n  const defaultTextColors = hasCustomTextColor\n    ? ''\n    : 'text-black dark:text-white';\n\n  const getPointerAnimation = (): PointerAnim => {\n    const startOffset = 15;\n    const endOffset = 5;\n    const yPos = dimensions.height / 2;\n\n    switch (direction) {\n      case 'left':\n        return {\n          initial: { x: -startOffset, y: yPos, rotate: 275 },\n          animate: { x: dimensions.width + endOffset, y: yPos, rotate: 275 },\n        };\n      case 'right':\n        return {\n          initial: { x: dimensions.width + startOffset, y: yPos, rotate: 5 },\n          animate: { x: -20, y: yPos, rotate: 5 },\n        };\n      case 'top':\n        return {\n          initial: { x: -startOffset, y: yPos, rotate: 275 },\n          animate: { x: dimensions.width + endOffset, y: yPos, rotate: 275 },\n        };\n      case 'bottom':\n        return {\n          initial: { x: -startOffset, y: yPos, rotate: 275 },\n          animate: { x: dimensions.width + endOffset, y: yPos, rotate: 275 },\n        };\n      default:\n        return {\n          initial: { x: -startOffset, y: yPos, rotate: 275 },\n          animate: { x: dimensions.width + endOffset, y: yPos, rotate: 275 },\n        };\n    }\n  };\n\n  const delay = showPointer ? pointerDuration + 0.2 : 0.2;\n\n  const directionStyles: Record<\n    'left' | 'right' | 'top' | 'bottom',\n    {\n      initial: { clipPath: string };\n      animate: { clipPath: string };\n      transition: { duration: number; ease: 'easeInOut'; delay: number };\n    }\n  > = {\n    left: {\n      initial: { clipPath: 'inset(0 100% 0 0)' },\n      animate: {\n        clipPath: isInView ? 'inset(0 0% 0 0)' : 'inset(0 100% 0 0)',\n      },\n      transition: { duration, ease: 'easeInOut', delay },\n    },\n    right: {\n      initial: { clipPath: 'inset(0 0 0 100%)' },\n      animate: {\n        clipPath: isInView ? 'inset(0 0 0 0%)' : 'inset(0 0 0 100%)',\n      },\n      transition: { duration, ease: 'easeInOut', delay },\n    },\n    top: {\n      initial: { clipPath: 'inset(100% 0 0 0)' },\n      animate: {\n        clipPath: isInView ? 'inset(0% 0 0 0)' : 'inset(100% 0 0 0)',\n      },\n      transition: { duration, ease: 'easeInOut', delay },\n    },\n    bottom: {\n      initial: { clipPath: 'inset(0 0 100% 0)' },\n      animate: {\n        clipPath: isInView ? 'inset(0 0 0% 0)' : 'inset(0 0 100% 0)',\n      },\n      transition: { duration, ease: 'easeInOut', delay },\n    },\n  };\n\n  const pointerAnimation = getPointerAnimation();\n  const gradientClass = `bg-linear-to-r ${gradient}`;\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn('relative inline-block', containerClassName)}\n    >\n      {rectangle && dimensions.width > 0 && dimensions.height > 0 && (\n        <motion.div\n          className={cn(\n            'absolute z-10 pointer-events-none',\n            'border border-neutral-800 dark:border-neutral-200 bg-transparent',\n            typeof rectangle === 'string' ? rectangle : '',\n          )}\n          style={{\n            left: direction === 'right' ? 'auto' : 0,\n            right: direction === 'right' ? 0 : 'auto',\n          }}\n          initial={{ width: 0, height: dimensions.height }}\n          animate={\n            isInView\n              ? { width: dimensions.width, height: dimensions.height }\n              : { width: 0, height: dimensions.height }\n          }\n          transition={{ duration: pointerDuration, ease: 'easeInOut' }}\n        />\n      )}\n\n      <div className='relative z-40'>\n        <span className={cn(defaultTextColors, 'relative z-20', className)}>\n          {content}\n        </span>\n\n        <motion.span\n          key={resetKey}\n          className={cn(\n            'absolute inset-0 bg-clip-text text-transparent z-30',\n            gradientClass,\n            className,\n          )}\n          initial={directionStyles[direction].initial}\n          animate={directionStyles[direction].animate}\n          transition={directionStyles[direction].transition}\n        >\n          {content}\n        </motion.span>\n      </div>\n\n      {showPointer && dimensions.width > 0 && dimensions.height > 0 && (\n        <motion.div\n          className='pointer-events-none absolute z-40'\n          initial={{ opacity: 0, ...pointerAnimation.initial }}\n          animate={\n            isInView\n              ? { opacity: 1, ...pointerAnimation.animate }\n              : { opacity: 0 }\n          }\n          transition={{\n            duration: pointerDuration,\n            ease: 'easeInOut',\n            opacity: { duration: 0.2, ease: 'easeInOut' },\n          }}\n        >\n          <Pointer className={cn('h-4 w-4 text-blue-500', pointerClassName)} />\n        </motion.div>\n      )}\n    </div>\n  );\n}\n"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
