{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "back-to-top",
  "title": "Back To Top",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A floating button that appears after scrolling, draws a progress ring, scrolls smoothly (instantly under reduced motion) and moves focus to the content.",
  "dependencies": [
    "lucide-react",
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/scroll.json",
    "https://ui.ballmac.com/r/motion-presets.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/back-to-top.tsx",
      "content": "// Ballmac UI: Back To Top. https://ui.ballmac.com/components/back-to-top\n\"use client\";\n\nimport * as React from \"react\";\nimport { ArrowUp } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion, useScroll, useTransform } from \"motion/react\";\nimport { prefersReducedMotion, useScrolled } from \"@/lib/ballmac/scroll\";\nimport { spring } from \"@/lib/ballmac/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { useMessages } from \"@/lib/ballmac/i18n\";\n\ntype BackToTopProps = Omit<React.ComponentProps<\"button\">, \"onClick\"> & {\n  /** Scroll distance in pixels after which the button appears. */\n  threshold?: number;\n  /** A scrollable element to watch and scroll instead of the page. */\n  container?: React.RefObject<HTMLElement | null>;\n  /** Draw a ring around the button that fills with scroll progress. */\n  showProgress?: boolean;\n  /** CSS selector of the element that receives focus after scrolling. Defaults to `main`, then the page. */\n  focusTarget?: string;\n  /** Button text, used as the accessible name. */\n  label?: string;\n  /** Show the label next to the arrow. */\n  showLabel?: boolean;\n};\n\nconst R = 18;\nconst C = 2 * Math.PI * R;\n\n/**\n * A floating button that appears after the reader scrolls down and returns them to the top. Focus moves to the main\n * content afterwards, so keyboard users do not lose their place when the button fades away.\n * Fixed to the viewport; add `absolute` to `className` to place it inside a positioned container.\n */\nfunction BackToTop({\n  threshold = 320,\n  container,\n  showProgress = true,\n  focusTarget,\n  label,\n  showLabel = false,\n  className,\n  ...props\n}: BackToTopProps) {\n  const msg = useMessages()\n  label ??= msg(\"back-to-top.label\", \"Back to top\")\n  const reduce = useReducedMotion();\n  const visible = useScrolled(threshold, container);\n  const { scrollYProgress } = useScroll(container ? { container } : undefined);\n  const dashOffset = useTransform(scrollYProgress, (p) => C * (1 - p));\n  return (\n    <AnimatePresence>\n      {visible && (\n        <motion.button\n          type=\"button\"\n          data-slot=\"back-to-top\"\n          aria-label={showLabel ? undefined : label}\n          initial={reduce ? { opacity: 0 } : { opacity: 0, y: 16, scale: 0.9 }}\n          animate={{ opacity: 1, y: 0, scale: 1 }}\n          exit={reduce ? { opacity: 0 } : { opacity: 0, y: 16, scale: 0.9 }}\n          transition={reduce ? { duration: 0.1 } : spring.snappy}\n          onClick={() => {\n            const behavior = prefersReducedMotion() ? \"auto\" : \"smooth\";\n            const box = container?.current;\n            if (box) box.scrollTo({ top: 0, behavior });\n            else window.scrollTo({ top: 0, behavior });\n            const target = document.querySelector<HTMLElement>(focusTarget ?? \"main\") ?? document.body;\n            if (!target.hasAttribute(\"tabindex\")) target.setAttribute(\"tabindex\", \"-1\");\n            target.focus({ preventScroll: true });\n          }}\n          className={cn(\n            \"fixed end-5 bottom-5 z-40 inline-flex h-11 items-center justify-center gap-2 rounded-full border bg-background/90 text-foreground shadow-[0_8px_24px_-8px_rgb(0_0_0/0.3)] outline-none backdrop-blur transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n            showLabel ? \"pe-4 ps-3\" : \"w-11\",\n            className,\n          )}\n          {...(props as object)}\n        >\n          {showProgress && !showLabel ? (\n            <svg aria-hidden=\"true\" viewBox=\"0 0 44 44\" className=\"pointer-events-none absolute inset-0 -rotate-90\">\n              <circle cx=\"22\" cy=\"22\" r={R} fill=\"none\" strokeWidth=\"2\" className=\"stroke-border\" />\n              <motion.circle\n                cx=\"22\"\n                cy=\"22\"\n                r={R}\n                fill=\"none\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                className=\"stroke-primary\"\n                strokeDasharray={C}\n                style={{ strokeDashoffset: dashOffset }}\n              />\n            </svg>\n          ) : null}\n          <ArrowUp aria-hidden=\"true\" className=\"relative size-4\" />\n          {showLabel && <span className=\"relative text-sm font-medium\">{label}</span>}\n        </motion.button>\n      )}\n    </AnimatePresence>\n  );\n}\n\nexport { BackToTop, type BackToTopProps };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/back-to-top.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "button",
      "scroll",
      "floating",
      "progress"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "Render <BackToTop /> once. threshold sets when it appears; container follows a scrollable element; focusTarget chooses where focus lands.",
      "whenToUse": [
        "Long pages, feeds and docs",
        "Panels with a lot of scrolling"
      ],
      "whenNotToUse": [
        "Short pages",
        "Infinite feeds where returning to the top loses work"
      ],
      "composesWith": [
        "scroll-progress",
        "button"
      ],
      "a11y": [
        {
          "keys": "Enter / Space",
          "action": "Scrolls to the top and moves focus to the main content so focus is not lost when the button disappears"
        },
        {
          "keys": "Reduced motion",
          "action": "Jumps instead of scrolling smoothly"
        }
      ],
      "customization": [
        "threshold",
        "showProgress",
        "showLabel",
        "container and focusTarget"
      ]
    },
    "examples": [
      "back-to-top-demo",
      "back-to-top-states"
    ],
    "url": "https://ui.ballmac.com/components/back-to-top"
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}