{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-to-top",
  "type": "registry:ui",
  "title": "Scroll To Top",
  "description": "Floating scroll-to-top button with 4 variants: default, pill, minimal, and progress indicator.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/scroll-to-top.tsx",
      "content": "\"use client\"\n\nimport { ArrowUp, ChevronUp } from \"lucide-react\"\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\ninterface ScrollToTopProps {\n  /** Scroll threshold in pixels before showing button */\n  threshold?: number\n  /** Scroll behavior */\n  behavior?: ScrollBehavior\n  /** Additional CSS classes */\n  className?: string\n  /** Animation duration in ms */\n  animationDuration?: number\n}\n\ninterface ScrollToTopDefaultProps extends ScrollToTopProps {\n  /** Button size */\n  size?: \"sm\" | \"md\" | \"lg\"\n}\n\ninterface ScrollToTopPillProps extends ScrollToTopProps {\n  /** Custom label text */\n  label?: string\n}\n\ninterface ScrollToTopProgressProps extends ScrollToTopProps {\n  /** Progress circle color */\n  progressColor?: string\n  /** Progress circle background color */\n  progressBgColor?: string\n  /** Progress circle stroke width */\n  strokeWidth?: number\n}\n\n/**\n * Hook to track scroll position and visibility\n */\nfunction useScrollToTop(threshold: number = 300) {\n  const [isVisible, setIsVisible] = React.useState(false)\n  const [scrollProgress, setScrollProgress] = React.useState(0)\n\n  React.useEffect(() => {\n    const handleScroll = () => {\n      const scrollTop = window.scrollY\n      const docHeight =\n        document.documentElement.scrollHeight - window.innerHeight\n\n      setIsVisible(scrollTop > threshold)\n      setScrollProgress(docHeight > 0 ? (scrollTop / docHeight) * 100 : 0)\n    }\n\n    // Initial check\n    handleScroll()\n\n    window.addEventListener(\"scroll\", handleScroll, { passive: true })\n    return () => window.removeEventListener(\"scroll\", handleScroll)\n  }, [threshold])\n\n  const scrollToTop = React.useCallback(\n    (behavior: ScrollBehavior = \"smooth\") => {\n      window.scrollTo({ top: 0, behavior })\n    },\n    [],\n  )\n\n  return { isVisible, scrollProgress, scrollToTop }\n}\n\n/**\n * ScrollToTop - Default variant\n *\n * Circular button with arrow icon\n */\nexport function ScrollToTop({\n  threshold = 300,\n  behavior = \"smooth\",\n  className,\n  size = \"md\",\n}: ScrollToTopDefaultProps) {\n  const { isVisible, scrollToTop } = useScrollToTop(threshold)\n  const [isAnimating, setIsAnimating] = React.useState(false)\n\n  const sizeClasses = {\n    sm: \"size-10\",\n    md: \"size-12\",\n    lg: \"size-14\",\n  }\n\n  const iconSizes = {\n    sm: \"size-4\",\n    md: \"size-5\",\n    lg: \"size-6\",\n  }\n\n  const handleClick = () => {\n    setIsAnimating(true)\n    scrollToTop(behavior)\n    setTimeout(() => setIsAnimating(false), 600)\n  }\n\n  return (\n    <button\n      type=\"button\"\n      onClick={handleClick}\n      aria-label=\"Scroll to top\"\n      className={cn(\n        \"fixed bottom-6 right-6 z-50 flex items-center justify-center rounded-full\",\n        \"bg-primary text-primary-foreground shadow-lg\",\n        \"transition-all duration-300 ease-out\",\n        \"hover:scale-110 hover:shadow-xl active:scale-95\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n        \"cursor-pointer\",\n        sizeClasses[size],\n        isVisible\n          ? \"translate-y-0 opacity-100\"\n          : \"pointer-events-none translate-y-4 opacity-0\",\n        className,\n      )}\n    >\n      {/* Click ripple animation */}\n      {isAnimating && (\n        <span\n          className=\"absolute inset-0 rounded-full animate-ping bg-primary-foreground/30\"\n          style={{ animationDuration: \"600ms\", animationIterationCount: 1 }}\n        />\n      )}\n      <ArrowUp\n        className={cn(\n          iconSizes[size],\n          \"relative z-10 transition-transform\",\n          isAnimating && \"animate-bounce\",\n        )}\n        style={isAnimating ? { animationDuration: \"500ms\" } : undefined}\n      />\n    </button>\n  )\n}\n\n/**\n * ScrollToTopPill - Pill variant\n *\n * Elongated button with text label\n */\nexport function ScrollToTopPill({\n  threshold = 300,\n  behavior = \"smooth\",\n  className,\n  label = \"Scroll to top\",\n}: ScrollToTopPillProps) {\n  const { isVisible, scrollToTop } = useScrollToTop(threshold)\n  const [isAnimating, setIsAnimating] = React.useState(false)\n\n  const handleClick = () => {\n    setIsAnimating(true)\n    scrollToTop(behavior)\n    setTimeout(() => setIsAnimating(false), 600)\n  }\n\n  return (\n    <button\n      type=\"button\"\n      onClick={handleClick}\n      aria-label={label}\n      className={cn(\n        \"fixed bottom-6 right-6 z-50 flex items-center gap-2 px-4 py-2.5 rounded-full\",\n        \"bg-primary text-primary-foreground text-sm font-medium shadow-lg\",\n        \"transition-all duration-300 ease-out\",\n        \"hover:shadow-xl hover:bg-primary/90 active:scale-95\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n        \"cursor-pointer\",\n        isVisible\n          ? \"translate-y-0 opacity-100\"\n          : \"pointer-events-none translate-y-4 opacity-0\",\n        className,\n      )}\n    >\n      {/* Click ripple animation */}\n      {isAnimating && (\n        <span\n          className=\"absolute inset-0 rounded-full animate-ping bg-primary-foreground/30\"\n          style={{ animationDuration: \"600ms\", animationIterationCount: 1 }}\n        />\n      )}\n      <ChevronUp\n        className={cn(\n          \"size-4 relative z-10 transition-transform\",\n          isAnimating && \"animate-bounce\",\n        )}\n        style={isAnimating ? { animationDuration: \"500ms\" } : undefined}\n      />\n      <span className=\"relative z-10\">{label}</span>\n    </button>\n  )\n}\n\n/**\n * ScrollToTopMinimal - Minimal variant\n *\n * Icon-only button without background\n */\nexport function ScrollToTopMinimal({\n  threshold = 300,\n  behavior = \"smooth\",\n  className,\n}: ScrollToTopProps) {\n  const { isVisible, scrollToTop } = useScrollToTop(threshold)\n\n  return (\n    <button\n      type=\"button\"\n      onClick={() => scrollToTop(behavior)}\n      aria-label=\"Scroll to top\"\n      className={cn(\n        \"fixed bottom-6 right-6 z-50 flex items-center justify-center size-10\",\n        \"text-muted-foreground\",\n        \"transition-all duration-300 ease-out\",\n        \"hover:text-foreground hover:scale-110 active:scale-95\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n        \"cursor-pointer\",\n        isVisible\n          ? \"translate-y-0 opacity-100\"\n          : \"pointer-events-none translate-y-4 opacity-0\",\n        className,\n      )}\n    >\n      <ArrowUp className=\"size-6\" />\n    </button>\n  )\n}\n\n/**\n * ScrollToTopProgress - Progress variant\n *\n * Circular button with scroll progress indicator\n */\nexport function ScrollToTopProgress({\n  threshold = 300,\n  behavior = \"smooth\",\n  className,\n  progressColor,\n  progressBgColor,\n  strokeWidth = 3,\n}: ScrollToTopProgressProps) {\n  const { isVisible, scrollProgress, scrollToTop } = useScrollToTop(threshold)\n  const [isAnimating, setIsAnimating] = React.useState(false)\n\n  // Circle dimensions\n  const size = 48\n  const radius = (size - strokeWidth) / 2\n  const circumference = 2 * Math.PI * radius\n  const strokeDashoffset =\n    circumference - (scrollProgress / 100) * circumference\n\n  const handleClick = () => {\n    setIsAnimating(true)\n    scrollToTop(behavior)\n    setTimeout(() => setIsAnimating(false), 600)\n  }\n\n  return (\n    <button\n      type=\"button\"\n      onClick={handleClick}\n      aria-label={`Scroll to top (${Math.round(scrollProgress)}% scrolled)`}\n      className={cn(\n        \"fixed bottom-6 right-6 z-50 flex items-center justify-center size-12\",\n        \"bg-background rounded-full shadow-lg\",\n        \"transition-all duration-300 ease-out\",\n        \"hover:scale-110 hover:shadow-xl active:scale-95\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n        \"cursor-pointer\",\n        isVisible\n          ? \"translate-y-0 opacity-100\"\n          : \"pointer-events-none translate-y-4 opacity-0\",\n        className,\n      )}\n    >\n      {/* Click ripple animation */}\n      {isAnimating && (\n        <span\n          className=\"absolute inset-0 rounded-full animate-ping bg-primary/30\"\n          style={{ animationDuration: \"600ms\", animationIterationCount: 1 }}\n        />\n      )}\n      {/* Progress circle SVG */}\n      <svg\n        className=\"absolute inset-0 -rotate-90\"\n        width={size}\n        height={size}\n        viewBox={`0 0 ${size} ${size}`}\n      >\n        {/* Background circle */}\n        <circle\n          cx={size / 2}\n          cy={size / 2}\n          r={radius}\n          fill=\"none\"\n          stroke={progressBgColor || \"currentColor\"}\n          strokeWidth={strokeWidth}\n          className={cn(!progressBgColor && \"text-muted\")}\n        />\n        {/* Progress circle */}\n        <circle\n          cx={size / 2}\n          cy={size / 2}\n          r={radius}\n          fill=\"none\"\n          stroke={progressColor || \"currentColor\"}\n          strokeWidth={strokeWidth}\n          strokeLinecap=\"round\"\n          strokeDasharray={circumference}\n          strokeDashoffset={strokeDashoffset}\n          className={cn(\n            \"transition-[stroke-dashoffset] duration-150 ease-out\",\n            !progressColor && \"text-primary\",\n          )}\n        />\n      </svg>\n      {/* Arrow icon with bounce animation on click */}\n      <ArrowUp\n        className={cn(\n          \"size-5 text-foreground relative z-10 transition-transform\",\n          isAnimating && \"animate-bounce\",\n        )}\n        style={isAnimating ? { animationDuration: \"500ms\" } : undefined}\n      />\n    </button>\n  )\n}\n\n/**\n * ScrollToTopText - Text variant with animated icon\n *\n * Minimal text button with animated circle progress\n */\nexport function ScrollToTopText({\n  threshold = 300,\n  behavior = \"smooth\",\n  className,\n  label = \"Scroll to top\",\n}: ScrollToTopPillProps) {\n  const { isVisible, scrollProgress, scrollToTop } = useScrollToTop(threshold)\n  const [isAnimating, setIsAnimating] = React.useState(false)\n\n  // Circle dimensions for viewBox 0 0 24 24\n  const radius = 10\n  const circumference = 2 * Math.PI * radius\n  const circleOffset = circumference - (scrollProgress / 100) * circumference\n  const arrowPathLength = 20\n\n  const handleClick = () => {\n    setIsAnimating(true)\n    scrollToTop(behavior)\n    setTimeout(() => setIsAnimating(false), 600)\n  }\n\n  return (\n    <button\n      type=\"button\"\n      onClick={handleClick}\n      aria-label={label}\n      className={cn(\n        \"group fixed bottom-6 right-6 z-50 flex items-center gap-1.5\",\n        \"text-muted-foreground hover:text-foreground\",\n        \"transition-all duration-300 ease-out\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n        \"cursor-pointer\",\n        isVisible\n          ? \"translate-y-0 opacity-100\"\n          : \"pointer-events-none translate-y-4 opacity-0\",\n        className,\n      )}\n    >\n      <span className=\"text-sm font-medium\">{label}</span>\n      <svg\n        width=\"20\"\n        height=\"20\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        className=\"overflow-visible\"\n      >\n        {/* Background circle */}\n        <circle\n          cx=\"12\"\n          cy=\"12\"\n          r={radius}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={1.5}\n          className=\"opacity-20\"\n        />\n        {/* Progress circle */}\n        <circle\n          cx=\"12\"\n          cy=\"12\"\n          r={radius}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={1.5}\n          strokeLinecap=\"round\"\n          strokeDasharray={circumference}\n          strokeDashoffset={circleOffset}\n          className=\"transition-all duration-500 ease-out\"\n          style={{ transform: \"rotate(-90deg)\", transformOrigin: \"center\" }}\n        />\n        {/* Arrow path */}\n        <path\n          d=\"M12 16V8M8 12l4-4 4 4\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          stroke=\"currentColor\"\n          strokeWidth={1.5}\n          strokeDasharray={arrowPathLength}\n          strokeDashoffset={isVisible ? 0 : arrowPathLength}\n          className={cn(\n            \"transition-all duration-500 ease-out\",\n            isAnimating && \"animate-pulse\",\n          )}\n        />\n      </svg>\n    </button>\n  )\n}\n\n// Named exports for convenience\nexport { useScrollToTop }\n",
      "type": "registry:ui"
    }
  ]
}