{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-areapro",
  "type": "registry:ui",
  "title": "ScrollArea Pro",
  "description": "Advanced scroll area component with horizontal and vertical scrolling, customizable styles, and smooth animations.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": [
    "@radix-ui/react-scroll-area",
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/scroll-areapro.tsx",
      "content": "'use client';\nimport * as React from 'react';\nimport * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';\nimport {\n  motion,\n  AnimatePresence,\n  useMotionValue,\n  useTransform,\n} from 'motion/react';\nimport { cn } from '@/lib/utils';\n\ninterface ScrollAreaProProps extends React.ComponentProps<\n  typeof ScrollAreaPrimitive.Root\n> {\n  className?: string;\n  children: React.ReactNode;\n  crossDirectionalScroll?: boolean;\n  autoHide?: boolean;\n  showProgress?: 'horizontal' | 'vertical';\n  onScrollChange?: (progress: { x: number; y: number }) => void;\n}\n\ninterface ScrollBarProProps extends React.ComponentProps<\n  typeof ScrollAreaPrimitive.ScrollAreaScrollbar\n> {\n  className?: string;\n  show?: boolean;\n  autoHide?: boolean;\n  isScrolling?: boolean;\n  orientation?: 'vertical' | 'horizontal';\n}\n\nconst isMobileDevice = (): boolean => {\n  if (typeof window !== 'undefined') {\n    if (window.innerWidth <= 1024) return true;\n    const hasTouchScreen =\n      'ontouchstart' in window || navigator.maxTouchPoints > 0;\n    const mobileUserAgent =\n      /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|Tablet/i.test(\n        navigator.userAgent,\n      );\n    if (hasTouchScreen && mobileUserAgent) return true;\n    if (window.innerWidth <= 1366 && hasTouchScreen) return true;\n  }\n  return false;\n};\n\nfunction ScrollBarPro({\n  className,\n  orientation = 'vertical',\n  show = true,\n  autoHide = false,\n  isScrolling = false,\n  ...props\n}: ScrollBarProProps) {\n  if (!autoHide && !isScrolling) {\n    return (\n      <ScrollAreaPrimitive.ScrollAreaScrollbar\n        data-slot='scroll-area-scrollbar'\n        orientation={orientation}\n        className={cn(\n          'flex touch-none p-px transition-colors select-none',\n          orientation === 'vertical' &&\n            'h-full w-2.5 border-l border-l-transparent',\n          orientation === 'horizontal' &&\n            'h-2.5 flex-col border-t border-t-transparent',\n          className,\n        )}\n        {...props}\n      >\n        <ScrollAreaPrimitive.ScrollAreaThumb\n          data-slot='scroll-area-thumb'\n          className='bg-border relative flex-1 rounded-full'\n        />\n      </ScrollAreaPrimitive.ScrollAreaScrollbar>\n    );\n  }\n\n  return (\n    <AnimatePresence>\n      {(!autoHide || show) && (\n        <motion.div\n          initial={{ opacity: autoHide ? 0 : 1 }}\n          animate={{ opacity: show ? 1 : 0 }}\n          exit={{ opacity: 0 }}\n          transition={{ duration: 0.3 }}\n        >\n          <ScrollAreaPrimitive.ScrollAreaScrollbar\n            data-slot='scroll-area-scrollbar'\n            orientation={orientation}\n            className={cn(\n              'flex touch-none select-none transition-all duration-300',\n              orientation === 'vertical' &&\n                'h-full border-l border-l-transparent',\n              orientation === 'horizontal' &&\n                'flex-col border-t border-t-transparent',\n              'p-0.5 xs:p-px',\n              orientation === 'vertical' &&\n                'w-1.5 xs:w-2 sm:w-2.5 md:w-2 lg:w-2.5',\n              orientation === 'horizontal' &&\n                'h-1.5 xs:h-2 sm:h-2.5 md:h-2 lg:h-2.5',\n              'hover:w-2 hover:h-2 xs:hover:w-2.5 xs:hover:h-2.5 sm:hover:w-3 sm:hover:h-3 md:hover:w-2.5 md:hover:h-2.5 lg:hover:w-3 lg:hover:h-3',\n              className,\n            )}\n            {...props}\n          >\n            <motion.div\n              animate={{\n                scale: isScrolling ? 1.1 : 1,\n                backgroundColor: isScrolling ? '#3b82f6' : '#d1d5db',\n              }}\n              transition={{ duration: 0.2 }}\n              className='relative flex-1 rounded-full'\n            >\n              <ScrollAreaPrimitive.ScrollAreaThumb\n                data-slot='scroll-area-thumb'\n                className='bg-current relative flex-1 rounded-full'\n              />\n            </motion.div>\n          </ScrollAreaPrimitive.ScrollAreaScrollbar>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction ScrollAreaPro({\n  className,\n  children,\n  crossDirectionalScroll = false,\n  autoHide = false,\n  showProgress,\n  onScrollChange,\n  ...props\n}: ScrollAreaProProps) {\n  const [isScrolling, setIsScrolling] = React.useState<boolean>(false);\n  const [showScrollbars, setShowScrollbars] =\n    React.useState<boolean>(!autoHide);\n  const [isFullyVisible, setIsFullyVisible] = React.useState<boolean>(true);\n  const [isMobile, setIsMobile] = React.useState<boolean>(false);\n  const [containerHeight, setContainerHeight] = React.useState(0);\n  const [isScrollingY, setIsScrollingY] = React.useState(false);\n  const scrollTimeoutVerticalRef = React.useRef<NodeJS.Timeout | null>(null);\n  const viewportRef = React.useRef<HTMLDivElement>(null);\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const scrollTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);\n\n  const scrollX = useMotionValue(0);\n  const scrollY = useMotionValue(0);\n  const progressBarScaleX = useTransform(scrollX, [0, 100], [0, 1]);\n  const progressBarScaleY = useTransform(scrollY, [0, 100], [0, 1]);\n\n  const verticalScrollProgress = useMotionValue(0);\n  const progressHeight = containerHeight - 20;\n  const y1 = useTransform(\n    verticalScrollProgress,\n    [0, 0.8],\n    [50, progressHeight],\n  );\n  const y2 = useTransform(\n    verticalScrollProgress,\n    [0, 1],\n    [50, Math.max(50, progressHeight - 200)],\n  );\n\n  React.useEffect(() => {\n    const checkMobile = () => {\n      setIsMobile(isMobileDevice());\n    };\n    checkMobile();\n\n    const updateContainerHeight = () => {\n      if (containerRef.current) {\n        setContainerHeight(containerRef.current.offsetHeight);\n      }\n    };\n\n    updateContainerHeight();\n\n    window.addEventListener('resize', checkMobile);\n    window.addEventListener('orientationchange', checkMobile);\n    window.addEventListener('resize', updateContainerHeight);\n\n    return () => {\n      window.removeEventListener('resize', checkMobile);\n      window.removeEventListener('orientationchange', checkMobile);\n      window.removeEventListener('resize', updateContainerHeight);\n    };\n  }, []);\n\n  const handleScroll = React.useCallback((): void => {\n    if (!viewportRef.current) return;\n    const element = viewportRef.current;\n    const maxScrollX = element.scrollWidth - element.clientWidth;\n    const maxScrollY = element.scrollHeight - element.clientHeight;\n    const progressX =\n      maxScrollX > 0 ? (element.scrollLeft / maxScrollX) * 100 : 0;\n    const progressY =\n      maxScrollY > 0 ? (element.scrollTop / maxScrollY) * 100 : 0;\n    scrollX.set(progressX);\n    scrollY.set(progressY);\n    verticalScrollProgress.set(progressY / 100);\n    onScrollChange?.({ x: progressX, y: progressY });\n    if (autoHide) {\n      setIsScrolling(true);\n      setShowScrollbars(true);\n      if (scrollTimeoutRef.current) {\n        clearTimeout(scrollTimeoutRef.current);\n      }\n      if (scrollTimeoutVerticalRef.current) {\n        clearTimeout(scrollTimeoutVerticalRef.current);\n      }\n      scrollTimeoutRef.current = setTimeout(() => {\n        setIsScrolling(false);\n        setShowScrollbars(false);\n      }, 1500);\n    }\n    if (showProgress === 'vertical') {\n      setIsScrollingY(true);\n      if (scrollTimeoutVerticalRef.current) {\n        clearTimeout(scrollTimeoutVerticalRef.current);\n      }\n      scrollTimeoutVerticalRef.current = setTimeout(() => {\n        setIsScrollingY(false);\n      }, 2000);\n    }\n  }, [\n    autoHide,\n    onScrollChange,\n    scrollX,\n    scrollY,\n    showProgress,\n    verticalScrollProgress,\n  ]);\n\n  const handleWheel = React.useCallback(\n    (e: WheelEvent): void => {\n      if (\n        !crossDirectionalScroll ||\n        !viewportRef.current ||\n        !isFullyVisible ||\n        isMobile\n      )\n        return;\n      if (Math.abs(e.deltaY) > 0) {\n        e.preventDefault();\n        viewportRef.current.scrollBy({\n          left: e.deltaY,\n          behavior: 'auto',\n        });\n      }\n    },\n    [crossDirectionalScroll, isFullyVisible, isMobile],\n  );\n\n  const handleTouchStart = React.useCallback(\n    (e: TouchEvent): void => {\n      if (\n        !crossDirectionalScroll ||\n        !viewportRef.current ||\n        !isFullyVisible ||\n        isMobile\n      )\n        return;\n      const touch = e.touches[0];\n      viewportRef.current.dataset.touchStartY = touch.clientY.toString();\n    },\n    [crossDirectionalScroll, isFullyVisible, isMobile],\n  );\n\n  const handleTouchMove = React.useCallback(\n    (e: TouchEvent): void => {\n      if (\n        !crossDirectionalScroll ||\n        !viewportRef.current ||\n        !isFullyVisible ||\n        isMobile\n      )\n        return;\n      const element = viewportRef.current;\n      const startY = parseFloat(element.dataset.touchStartY || '0');\n      const touch = e.touches[0];\n      const deltaY = touch.clientY - startY;\n      if (Math.abs(deltaY) > 10) {\n        e.preventDefault();\n        const currentScrollLeft = element.scrollLeft;\n        const maxScrollLeft = element.scrollWidth - element.clientWidth;\n        const scrollAmount = -deltaY * 2;\n        const newScrollLeft = Math.max(\n          0,\n          Math.min(maxScrollLeft, currentScrollLeft + scrollAmount),\n        );\n        element.scrollTo({ left: newScrollLeft, behavior: 'auto' });\n        element.dataset.touchStartY = touch.clientY.toString();\n      }\n    },\n    [crossDirectionalScroll, isFullyVisible, isMobile],\n  );\n\n  React.useEffect(() => {\n    const element = viewportRef.current;\n    if (!element) return;\n    element.addEventListener('scroll', handleScroll, { passive: true });\n    if (crossDirectionalScroll && isFullyVisible && !isMobile) {\n      element.addEventListener('wheel', handleWheel, { passive: false });\n      element.addEventListener('touchstart', handleTouchStart, {\n        passive: false,\n      });\n      element.addEventListener('touchmove', handleTouchMove, {\n        passive: false,\n      });\n    }\n    return () => {\n      element.removeEventListener('scroll', handleScroll);\n      if (crossDirectionalScroll && !isMobile) {\n        element.removeEventListener('wheel', handleWheel);\n        element.removeEventListener('touchstart', handleTouchStart);\n        element.removeEventListener('touchmove', handleTouchMove);\n      }\n      if (scrollTimeoutRef.current) {\n        clearTimeout(scrollTimeoutRef.current);\n      }\n    };\n  }, [\n    handleScroll,\n    handleWheel,\n    handleTouchStart,\n    handleTouchMove,\n    crossDirectionalScroll,\n    isFullyVisible,\n    isMobile,\n  ]);\n\n  React.useEffect(() => {\n    if (!crossDirectionalScroll || isMobile) return;\n    const container = containerRef.current;\n    if (!container) return;\n    const observer = new IntersectionObserver(\n      ([entry]) => setIsFullyVisible(entry.intersectionRatio >= 0.9),\n      { threshold: 0.9 },\n    );\n    observer.observe(container);\n    return () => observer.disconnect();\n  }, [crossDirectionalScroll, isMobile]);\n\n  React.useEffect(() => {\n    const resizeObserver = new ResizeObserver(() => {\n      if (containerRef.current) {\n        setContainerHeight(containerRef.current.offsetHeight);\n      }\n    });\n\n    if (containerRef.current) {\n      resizeObserver.observe(containerRef.current);\n    }\n\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, []);\n\n  return (\n    <ScrollAreaPrimitive.Root\n      ref={containerRef}\n      data-slot='scroll-area'\n      className={cn('relative', className)}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        ref={viewportRef}\n        data-slot='scroll-area-viewport'\n        className={cn(\n          'focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-hidden focus-visible:ring-[3px] focus-visible:outline-1',\n          'max-w-full min-w-0 min-h-0',\n          crossDirectionalScroll && !isMobile\n            ? 'overflow-x-auto overflow-y-hidden'\n            : 'overflow-auto',\n          crossDirectionalScroll &&\n            !isFullyVisible &&\n            !isMobile &&\n            'opacity-75',\n        )}\n      >\n        {children}\n      </ScrollAreaPrimitive.Viewport>\n\n      {(!crossDirectionalScroll || isMobile) && (\n        <ScrollBarPro\n          orientation='vertical'\n          show={showScrollbars}\n          autoHide={autoHide}\n          isScrolling={isScrolling}\n        />\n      )}\n      <ScrollBarPro\n        orientation='horizontal'\n        show={showScrollbars}\n        autoHide={autoHide}\n        isScrolling={isScrolling}\n      />\n      <ScrollAreaPrimitive.Corner />\n\n      <AnimatePresence>\n        {showProgress === 'vertical' && isScrollingY && containerHeight > 0 && (\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 0.3 }}\n            className='absolute top-3 right-0 flex items-start pointer-events-none'\n          >\n            <motion.div\n              initial={{ boxShadow: 'rgba(0, 0, 0, 0.24) 0px 3px 8px' }}\n              animate={{\n                boxShadow:\n                  verticalScrollProgress.get() > 0\n                    ? 'none'\n                    : 'rgba(0, 0, 0, 0.24) 0px 3px 8px',\n              }}\n              transition={{ duration: 0.2, delay: 0.5 }}\n              className='border-neutral-200 flex h-4 w-4 items-center justify-center rounded-full border shadow-xs'\n            >\n              <motion.div\n                initial={{ backgroundColor: '#10b981', borderColor: '#059669' }}\n                animate={{\n                  backgroundColor:\n                    verticalScrollProgress.get() > 0 ? 'white' : '#10b981',\n                  borderColor:\n                    verticalScrollProgress.get() > 0 ? 'white' : '#059669',\n                }}\n                transition={{ duration: 0.2, delay: 0.5 }}\n                className='h-2 w-2 rounded-full border border-neutral-300 bg-white'\n              />\n            </motion.div>\n\n            <svg\n              viewBox={`0 0 20 ${progressHeight}`}\n              width='20'\n              height={progressHeight}\n              className='block'\n              aria-hidden='true'\n            >\n              <motion.path\n                d={`M 10 0 V ${progressHeight}`}\n                fill='none'\n                stroke='#9091A0'\n                strokeOpacity='0.16'\n              />\n              <motion.path\n                d={`M 10 0 V ${progressHeight}`}\n                fill='none'\n                stroke='url(#gradient)'\n                strokeWidth='1.25'\n                className='motion-reduce:hidden'\n              />\n              <defs>\n                <motion.linearGradient\n                  id='gradient'\n                  gradientUnits='userSpaceOnUse'\n                  x1='0'\n                  x2='0'\n                  y1={y1}\n                  y2={y2}\n                >\n                  <stop stopColor='#18CCFC' stopOpacity='0' />\n                  <stop stopColor='#18CCFC' />\n                  <stop offset='0.325' stopColor='#6344F5' />\n                  <stop offset='1' stopColor='#AE48FF' stopOpacity='0' />\n                </motion.linearGradient>\n              </defs>\n            </svg>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      <AnimatePresence>\n        {showProgress === 'horizontal' && (\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 0.7 }}\n            exit={{ opacity: 0 }}\n            className='absolute inset-0 pointer-events-none'\n          >\n            <motion.div\n              className='absolute bottom-0 left-0 right-0 bg-gray-200/50 dark:bg-gray-700/50'\n              style={{ height: 'clamp(2px, 0.3vw, 6px)' }}\n              initial={{ scaleX: 0 }}\n              animate={{ scaleX: 1 }}\n              transition={{ delay: 0.2 }}\n            >\n              <motion.div\n                className='h-full bg-linear-to-r from-blue-500 to-purple-500 origin-left'\n                style={{ scaleX: progressBarScaleX }}\n              />\n            </motion.div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </ScrollAreaPrimitive.Root>\n  );\n}\n\nexport { ScrollAreaPro, ScrollBarPro };\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}"
    }
  ]
}
