{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "back-top",
  "title": "Back Top",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/back-top/BackTop.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { ArrowUp } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { backTopVariants } from './back-top.variants'\n\n// Inlined union: SFC compiler can't extract runtime props from\n// `BackTopVariants['size']` / `BackTopVariants['position']`.\nconst props = withDefaults(\n  defineProps<{\n    class?: HTMLAttributes['class']\n    /** Visibility threshold in pixels. Button appears once scroll passes it. */\n    threshold?: number\n    /** Target container. Defaults to the window. Pass a CSS selector or an HTMLElement. */\n    target?: string | HTMLElement | Window\n    /** Scroll behavior: 'smooth' or 'auto' (instant). */\n    behavior?: ScrollBehavior\n    /** Size variant. */\n    size?: 'sm' | 'default' | 'lg'\n    /** Edge anchor position. */\n    position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'\n    /** Distance from the viewport edge (px). */\n    offset?: number\n    /** Use absolute positioning (for section-level containers) instead of fixed (viewport). */\n    absolute?: boolean\n    /** Accessible label. */\n    ariaLabel?: string\n  }>(),\n  {\n    threshold: 200,\n    target: () => (typeof window !== 'undefined' ? window : undefined),\n    behavior: 'smooth',\n    size: 'default',\n    position: 'bottom-right',\n    offset: 24,\n    absolute: false,\n    ariaLabel: 'Scroll to top',\n  },\n)\n\nconst emit = defineEmits<{\n  visible: [value: boolean]\n  click: [event: MouseEvent]\n}>()\n\nconst visible = ref(false)\n\nfunction resolveTarget(): HTMLElement | Window | null {\n  if (typeof window === 'undefined') return null\n  if (props.target === undefined || props.target === null) return window\n  if (typeof props.target === 'string') {\n    const el = document.querySelector<HTMLElement>(props.target)\n    return el ?? window\n  }\n  return props.target\n}\n\nfunction getScrollTop(el: HTMLElement | Window): number {\n  if (el === window) {\n    return window.scrollY ?? document.documentElement.scrollTop ?? document.body.scrollTop ?? 0\n  }\n  return (el as HTMLElement).scrollTop\n}\n\nfunction scrollToTop(el: HTMLElement | Window) {\n  if (el === window) {\n    window.scrollTo({ top: 0, behavior: props.behavior })\n  } else {\n    ;(el as HTMLElement).scrollTo({ top: 0, behavior: props.behavior })\n  }\n}\n\nlet currentTarget: HTMLElement | Window | null = null\n\nfunction handleScroll() {\n  if (!currentTarget) return\n  const scrollTop = getScrollTop(currentTarget)\n  const next = scrollTop > props.threshold\n  if (next !== visible.value) {\n    visible.value = next\n    emit('visible', next)\n  }\n}\n\nfunction handleClick(e: MouseEvent) {\n  emit('click', e)\n  if (currentTarget) scrollToTop(currentTarget)\n}\n\nconst positionStyle = computed(() => {\n  const o = `${props.offset}px`\n  switch (props.position) {\n    case 'bottom-left':\n      return { left: o, bottom: o }\n    case 'top-right':\n      return { right: o, top: o }\n    case 'top-left':\n      return { left: o, top: o }\n    default:\n      return { right: o, bottom: o }\n  }\n})\n\nonMounted(() => {\n  currentTarget = resolveTarget()\n  if (!currentTarget) return\n  const listenEl: HTMLElement | Window = currentTarget\n  listenEl.addEventListener('scroll', handleScroll, { passive: true })\n  // If target is a container, also listen to window scroll for safety on resize.\n  if (currentTarget !== window) {\n    window.addEventListener('scroll', handleScroll, { passive: true })\n  }\n  handleScroll()\n})\n\nonBeforeUnmount(() => {\n  if (currentTarget) {\n    currentTarget.removeEventListener('scroll', handleScroll)\n    if (currentTarget !== window) {\n      window.removeEventListener('scroll', handleScroll)\n    }\n  }\n})\n\nwatch(\n  () => props.target,\n  () => {\n    if (currentTarget) {\n      currentTarget.removeEventListener('scroll', handleScroll)\n      if (currentTarget !== window) {\n        window.removeEventListener('scroll', handleScroll)\n      }\n    }\n    currentTarget = resolveTarget()\n    if (currentTarget) {\n      currentTarget.addEventListener('scroll', handleScroll, { passive: true })\n      if (currentTarget !== window) {\n        window.addEventListener('scroll', handleScroll, { passive: true })\n      }\n    }\n    handleScroll()\n  },\n)\n</script>\n\n<template>\n  <Transition name=\"back-top\">\n    <button\n      v-show=\"visible\"\n      data-uipkge\n      data-slot=\"back-top\"\n      :data-state=\"visible ? 'open' : 'closed'\"\n      :data-size=\"size\"\n      :data-position=\"position\"\n      type=\"button\"\n      :aria-label=\"ariaLabel\"\n      :class=\"cn(backTopVariants({ size, position }), props.absolute ? 'absolute' : 'fixed', props.class)\"\n      :style=\"positionStyle\"\n      @click=\"handleClick\"\n    >\n      <slot name=\"icon\">\n        <ArrowUp aria-hidden=\"true\" />\n      </slot>\n    </button>\n  </Transition>\n</template>\n\n<style scoped>\n.back-top-enter-active,\n.back-top-leave-active {\n  transition:\n    opacity 0.2s ease,\n    transform 0.2s ease;\n}\n.back-top-enter-from,\n.back-top-leave-to {\n  opacity: 0;\n  transform: scale(0.9);\n}\n</style>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/back-top/BackTop.vue"
    },
    {
      "path": "packages/registry-vue/components/back-top/back-top.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so `BackTop.vue` can `import { backTopVariants } from\n * './back-top.variants'` without creating a circular dependency through the\n * index. See card.variants.ts for the same pattern + the SSR symptom that\n * motivated the split.\n */\nexport const backTopVariants = cva(\n  'inline-flex items-center justify-center rounded-full border bg-background text-foreground shadow-lg transition-all duration-200 hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none cursor-pointer motion-safe:data-[state=open]:animate-in motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=closed]:zoom-out-95 [&_svg:not([class*=size-])]:size-5 [&_svg]:pointer-events-none [&_svg]:shrink-0',\n  {\n    variants: {\n      size: {\n        sm: 'size-8 [&_svg:not([class*=size-])]:size-4',\n        default: 'size-10',\n        lg: 'size-12 [&_svg:not([class*=size-])]:size-6',\n      },\n      position: {\n        'bottom-right': '',\n        'bottom-left': '',\n        'top-right': '',\n        'top-left': '',\n      },\n    },\n    defaultVariants: {\n      size: 'default',\n      position: 'bottom-right',\n    },\n  },\n)\n\nexport type BackTopVariants = VariantProps<typeof backTopVariants>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/back-top/back-top.variants.ts"
    },
    {
      "path": "packages/registry-vue/components/back-top/index.ts",
      "content": "export { default as BackTop } from './BackTop.vue'\n\n// Re-export variant API from the sibling file (kept separate to avoid the\n// BackTop.vue <-> index.ts circular import that broke dev SSR for Card).\nexport { backTopVariants, type BackTopVariants } from './back-top.variants'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/back-top/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Scroll-to-top floating button. Appears after the target container scrolls past a threshold and smooth-scrolls back on click. Supports custom target container, visibility threshold, scroll behavior, custom icon slot, four edge anchors, size variants, and edge offset.",
  "categories": [
    "navigation",
    "utility"
  ]
}