+ "content": "\"use client\";\r\n\r\nimport { useEffect, useState, useCallback } from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nimport { useMobile } from \"@/hooks/use-mobile\";\r\n\r\nexport interface ScrollIndicatorProps {\r\n showOnMobile?: boolean;\r\n showCornerIndicator?: boolean;\r\n showTopBar?: boolean;\r\n topBarClassName?: string;\r\n topBarGradient?: string;\r\n indicatorClassName?: string;\r\n indicatorSvgClassName?: string;\r\n indicatorPercentageClassName?: string;\r\n indicatorArrowClassName?: string;\r\n}\r\n\r\nexport function ScrollIndicator({\r\n showOnMobile = false,\r\n showCornerIndicator = true,\r\n showTopBar = false,\r\n topBarClassName,\r\n topBarGradient,\r\n indicatorClassName,\r\n indicatorSvgClassName,\r\n indicatorPercentageClassName,\r\n indicatorArrowClassName,\r\n}: ScrollIndicatorProps) {\r\n const [scrollPercentage, setScrollPercentage] = useState(0);\r\n const [isVisible, setIsVisible] = useState(false);\r\n const [isHovered, setIsHovered] = useState(false);\r\n const isMobile = useMobile();\r\n\r\n const calculateScrollPercentage = useCallback(() => {\r\n const windowHeight = window.innerHeight;\r\n const documentHeight = document.documentElement.scrollHeight - windowHeight;\r\n const scrolled = window.scrollY;\r\n const percentage = (scrolled / documentHeight) * 100;\r\n\r\n setScrollPercentage(Math.min(100, Math.max(0, percentage)));\r\n setIsVisible(scrolled > windowHeight - windowHeight / 3);\r\n }, []);\r\n\r\n useEffect(() => {\r\n calculateScrollPercentage();\r\n\r\n let ticking = false;\r\n const handleScroll = () => {\r\n if (!ticking) {\r\n window.requestAnimationFrame(() => {\r\n calculateScrollPercentage();\r\n ticking = false;\r\n });\r\n ticking = true;\r\n }\r\n };\r\n\r\n window.addEventListener(\"scroll\", handleScroll, { passive: true });\r\n window.addEventListener(\"resize\", calculateScrollPercentage, { passive: true });\r\n\r\n return () => {\r\n window.removeEventListener(\"scroll\", handleScroll);\r\n window.removeEventListener(\"resize\", calculateScrollPercentage);\r\n };\r\n }, [calculateScrollPercentage]);\r\n\r\n const scrollToTop = () => {\r\n window.scrollTo({\r\n top: 0,\r\n behavior: \"smooth\",\r\n });\r\n };\r\n\r\n const circumference = 2 * Math.PI * 42; // radius = 42 : 2*pi*r\r\n const strokeDashoffset = circumference - (scrollPercentage / 100) * circumference;\r\n\r\n if (isMobile && !showOnMobile) {\r\n return null;\r\n }\r\n\r\n return (\r\n <>\r\n {showTopBar && (\r\n <div className={cn(\"fixed top-0 left-0 z-50 h-1 w-full bg-gray-900/20\", topBarClassName)}>\r\n <div\r\n className={cn(\r\n \"h-full bg-gradient-to-r from-cyan-300 to-cyan-600 transition-all duration-300 ease-out\",\r\n topBarGradient\r\n )}\r\n style={{ width: `${scrollPercentage}%` }}\r\n />\r\n </div>\r\n )}\r\n\r\n {showCornerIndicator && (\r\n <button\r\n onClick={scrollToTop}\r\n onMouseEnter={() => setIsHovered(true)}\r\n onMouseLeave={() => setIsHovered(false)}\r\n className={cn(\r\n \"fixed right-4 bottom-4 z-50 flex h-15 w-15 items-center justify-center rounded-full bg-zinc-900 shadow-2xl transition-all duration-500 ease-out hover:scale-110\",\r\n isVisible\r\n ? \"translate-x-0 opacity-100\"\r\n : \"pointer-events-none translate-x-32 opacity-0\",\r\n indicatorClassName\r\n )}\r\n aria-label=\"Scroll to top\"\r\n >\r\n {/* SVG Circle Progress */}\r\n <svg className=\"absolute inset-0 h-full w-full -rotate-90\" viewBox=\"0 0 100 100\">\r\n <circle\r\n cx=\"50\"\r\n cy=\"50\"\r\n r=\"42\"\r\n fill=\"none\"\r\n stroke=\"rgba(255, 255, 255, 0.1)\"\r\n strokeWidth=\"4\"\r\n />\r\n {/* Progress circle */}\r\n <circle\r\n cx=\"50\"\r\n cy=\"50\"\r\n r=\"42\"\r\n fill=\"none\"\r\n strokeWidth=\"4\"\r\n strokeLinecap=\"round\"\r\n strokeDasharray={circumference}\r\n strokeDashoffset={strokeDashoffset}\r\n className={cn(\r\n \"stroke-cyan-400 transition-all duration-300 ease-out\",\r\n indicatorSvgClassName\r\n )}\r\n />\r\n </svg>\r\n\r\n <div className=\"relative flex items-center justify-center\">\r\n <span\r\n className={cn(\r\n \"text-sm font-bold text-white transition-all duration-300\",\r\n isHovered ? \"scale-0 opacity-0\" : \"scale-100 opacity-100\",\r\n indicatorPercentageClassName\r\n )}\r\n >\r\n {Math.round(scrollPercentage)}%\r\n </span>\r\n\r\n <svg\r\n className={cn(\r\n \"absolute h-6 w-6 text-cyan-400 transition-all duration-300\",\r\n isHovered ? \"scale-100 opacity-100\" : \"scale-0 opacity-0\",\r\n indicatorArrowClassName\r\n )}\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n strokeWidth={2}\r\n >\r\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 10l7-7m0 0l7 7m-7-7v18\" />\r\n </svg>\r\n </div>\r\n </button>\r\n )}\r\n </>\r\n );\r\n}\r\n\r\n// DevelopedBy: AetherUI\r\n",
0 commit comments