{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "anchor",
  "title": "Anchor",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/anchor/Anchor.vue",
      "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, provide, ref, toRef, watch } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport type { AnchorItem } from '.'\nimport { ANCHOR_INJECTION_KEY } from './context'\nimport AnchorLink from './AnchorLink.vue'\n\nconst props = withDefaults(\n  defineProps<{\n    items?: AnchorItem[]\n    offsetTop?: number\n    bounds?: number\n    scrollContainer?: HTMLElement | string | null\n    affix?: boolean\n    class?: HTMLAttributes['class']\n  }>(),\n  {\n    items: () => [],\n    offsetTop: 0,\n    bounds: 5,\n    scrollContainer: null,\n    affix: false,\n  },\n)\n\nconst emits = defineEmits<{\n  (e: 'change', href: string): void\n}>()\n\nconst activeHref = ref<string | null>(null)\nconst registered = ref<Set<string>>(new Set())\nconst resolvedContainer = ref<HTMLElement | Window>(typeof window !== 'undefined' ? window : (null as any))\n\nfunction resolveContainer(): HTMLElement | Window {\n  if (!props.scrollContainer) return window\n  if (typeof props.scrollContainer === 'string') {\n    return (document.querySelector(props.scrollContainer) as HTMLElement) ?? window\n  }\n  return props.scrollContainer\n}\n\nlet observer: IntersectionObserver | null = null\nconst observed = new Map<string, Element>()\n\nfunction rebuildObserver() {\n  if (observer) {\n    observer.disconnect()\n    observed.clear()\n  }\n  const root = resolvedContainer.value === window ? null : (resolvedContainer.value as HTMLElement)\n  observer = new IntersectionObserver(\n    (entries) => {\n      const intersecting = entries.filter((e) => e.isIntersecting)\n      if (intersecting.length === 0) return\n      intersecting.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)\n      const id = '#' + intersecting[0]!.target.id\n      if (id !== activeHref.value) {\n        activeHref.value = id\n        emits('change', id)\n      }\n    },\n    {\n      root,\n      rootMargin: `-${props.offsetTop}px 0px -60% 0px`,\n      threshold: [0, 1],\n    },\n  )\n  for (const href of registered.value) {\n    const el = document.querySelector(href)\n    if (el) {\n      observed.set(href, el)\n      observer.observe(el)\n    }\n  }\n}\n\nfunction setActive(href: string) {\n  activeHref.value = href\n  emits('change', href)\n}\n\nfunction register(href: string) {\n  registered.value.add(href)\n  if (!observer) return\n  const el = document.querySelector(href)\n  if (el && !observed.has(href)) {\n    observed.set(href, el)\n    observer.observe(el)\n  }\n}\n\nfunction unregister(href: string) {\n  registered.value.delete(href)\n  const el = observed.get(href)\n  if (el && observer) observer.unobserve(el)\n  observed.delete(href)\n}\n\nprovide(ANCHOR_INJECTION_KEY, {\n  activeHref,\n  setActive,\n  register,\n  unregister,\n  scrollContainer: resolvedContainer,\n  offsetTop: toRef(props, 'offsetTop'),\n})\n\nonMounted(() => {\n  resolvedContainer.value = resolveContainer()\n  rebuildObserver()\n})\n\nonBeforeUnmount(() => {\n  observer?.disconnect()\n  observer = null\n})\n\nwatch(\n  () => props.scrollContainer,\n  () => {\n    resolvedContainer.value = resolveContainer()\n    rebuildObserver()\n  },\n)\n</script>\n\n<template>\n  <nav\n    data-uipkge\n    data-slot=\"anchor\"\n    aria-label=\"Table of contents\"\n    :class=\"cn('border-border flex flex-col gap-1 border-l text-sm', affix && 'sticky', props.class)\"\n    :style=\"affix ? { top: `${offsetTop}px` } : undefined\"\n  >\n    <template v-if=\"items.length\">\n      <AnchorLink v-for=\"item in items\" :key=\"item.href\" :href=\"item.href\" :title=\"item.title\">\n        <AnchorLink v-for=\"child in item.children ?? []\" :key=\"child.href\" :href=\"child.href\" :title=\"child.title\" />\n      </AnchorLink>\n    </template>\n    <slot v-else />\n  </nav>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/anchor/Anchor.vue"
    },
    {
      "path": "packages/registry-vue/components/anchor/AnchorLink.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, inject, onBeforeUnmount, onMounted, useSlots } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { ANCHOR_INJECTION_KEY } from './context'\n\nconst props = defineProps<{\n  href: string\n  title: string\n  class?: HTMLAttributes['class']\n}>()\n\nconst _maybeCtx = inject(ANCHOR_INJECTION_KEY, null)\nif (!_maybeCtx) throw new Error('AnchorLink must be used inside <Anchor>.')\nconst ctx: NonNullable<typeof _maybeCtx> = _maybeCtx\n\nconst isActive = computed(() => ctx.activeHref.value === props.href)\nconst slots = useSlots()\n\nonMounted(() => ctx.register(props.href))\nonBeforeUnmount(() => ctx.unregister(props.href))\n\nfunction onClick(e: MouseEvent) {\n  e.preventDefault()\n  const el = document.querySelector(props.href) as HTMLElement | null\n  if (!el) return\n  const offset = ctx.offsetTop.value\n  const container = ctx.scrollContainer.value\n  const smooth = !window.matchMedia('(prefers-reduced-motion: reduce)').matches\n  const behavior: ScrollBehavior = smooth ? 'smooth' : 'auto'\n  if (container === window) {\n    const top = el.getBoundingClientRect().top + window.scrollY - offset\n    window.scrollTo({ top, behavior })\n  } else {\n    const c = container as HTMLElement\n    const top = el.getBoundingClientRect().top - c.getBoundingClientRect().top + c.scrollTop - offset\n    c.scrollTo({ top, behavior })\n  }\n  history.replaceState(null, '', props.href)\n  ctx.setActive(props.href)\n}\n</script>\n\n<template>\n  <div data-uipkge data-slot=\"anchor-link\" class=\"flex flex-col\">\n    <a\n      :href=\"href\"\n      :aria-current=\"isActive ? 'location' : undefined\"\n      :class=\"\n        cn(\n          'text-muted-foreground -ml-px block border-l-2 border-transparent py-2 pl-3 transition-colors',\n          'hover:text-foreground',\n          'focus-visible:ring-ring rounded-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',\n          isActive && 'border-primary text-foreground font-medium',\n          props.class,\n        )\n      \"\n      @click=\"onClick\"\n    >\n      {{ title }}\n    </a>\n    <div v-if=\"slots.default\" class=\"ml-3\">\n      <slot />\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/anchor/AnchorLink.vue"
    },
    {
      "path": "packages/registry-vue/components/anchor/context.ts",
      "content": "import type { InjectionKey, Ref } from 'vue'\n\nexport interface AnchorContext {\n  activeHref: Ref<string | null>\n  setActive: (href: string) => void\n  register: (href: string) => void\n  unregister: (href: string) => void\n  scrollContainer: Ref<HTMLElement | Window>\n  offsetTop: Ref<number>\n}\n\nexport const ANCHOR_INJECTION_KEY: InjectionKey<AnchorContext> = Symbol('uipkge-anchor')\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/anchor/context.ts"
    },
    {
      "path": "packages/registry-vue/components/anchor/index.ts",
      "content": "export interface AnchorItem {\n  href: string\n  title: string\n  children?: AnchorItem[]\n}\n\nexport { default as Anchor } from './Anchor.vue'\nexport { default as AnchorLink } from './AnchorLink.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/anchor/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "In-page navigation list with scroll-spy. Renders a vertical list of links; the active item highlights as the user scrolls through anchored sections.",
  "categories": [
    "navigation"
  ]
}