{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "popover",
  "title": "Popover",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/popover/Popover.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { PopoverCloseBehavior } from './context'\n\n  export interface PopoverProps {\n    /** Controlled open state (`bind:open`). Omit for uncontrolled. */\n    open?: boolean\n    defaultOpen?: boolean\n    /** Modal popovers trap Tab focus while open. Default false. */\n    modal?: boolean\n    /** Persist open state to localStorage. `true` auto-keys, a string sets the key. */\n    persist?: string | boolean\n    closeBehavior?: PopoverCloseBehavior\n    onOpenChange?: (open: boolean) => void\n    children?: Snippet\n  }\n</script>\n\n<script lang=\"ts\">\n  import { setContext } from 'svelte'\n  import { POPOVER_CONTEXT_KEY, type PopoverContextValue } from './context'\n\n  let {\n    defaultOpen = false,\n    open = $bindable(defaultOpen),\n    modal = false,\n    persist = false,\n    closeBehavior = 'auto',\n    onOpenChange,\n    children,\n  }: PopoverProps = $props()\n\n  const autoId = $props.id()\n\n  const storageKey = $derived(\n    persist === false || persist === undefined\n      ? null\n      : persist === true\n        ? `uipkge-popover-${autoId}`\n        : persist,\n  )\n\n  // Read the persisted value on the first client pass, then write on change.\n  // Runs only in the browser ($effect never fires during SSR), matching the\n  // Vue twin's onMounted read (no hydration mismatch from a server read).\n  let didInitPersist = false\n  $effect(() => {\n    const key = storageKey\n    if (key && typeof localStorage !== 'undefined') {\n      if (!didInitPersist) {\n        didInitPersist = true\n        if (localStorage.getItem(key) === '1') {\n          setOpen(true)\n          return\n        }\n      }\n      if (open) localStorage.setItem(key, '1')\n      else localStorage.removeItem(key)\n    }\n  })\n\n  function setOpen(value: boolean) {\n    open = value\n    onOpenChange?.(value)\n  }\n\n  let triggerEl: HTMLElement | null = null\n  let anchorEl: HTMLElement | null = null\n\n  const ctx: PopoverContextValue = {\n    id: autoId,\n    contentId: `${autoId}-content`,\n    triggerId: `${autoId}-trigger`,\n    isOpen: () => open,\n    setOpen,\n    toggle: () => setOpen(!open),\n    isModal: () => modal,\n    getCloseBehavior: () => closeBehavior,\n    getTriggerEl: () => triggerEl,\n    getAnchorEl: () => anchorEl,\n    // First registration wins: a Close trigger nested inside the content must\n    // not steal the positioning anchor from the outer trigger.\n    registerTrigger: (el) => {\n      if (!triggerEl) triggerEl = el\n    },\n    unregisterTrigger: (el) => {\n      if (triggerEl === el) triggerEl = null\n    },\n    registerAnchor: (el) => {\n      if (!anchorEl) anchorEl = el\n    },\n    unregisterAnchor: (el) => {\n      if (anchorEl === el) anchorEl = null\n    },\n  }\n  setContext(POPOVER_CONTEXT_KEY, ctx)\n</script>\n\n<span data-uipkge=\"\" data-slot=\"popover\" class=\"contents\">\n  {@render children?.()}\n</span>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/popover/Popover.svelte"
    },
    {
      "path": "packages/registry-svelte/components/popover/PopoverAnchor.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface PopoverAnchorProps extends HTMLAttributes<HTMLSpanElement> {\n    ref?: HTMLSpanElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { getContext } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { POPOVER_CONTEXT_KEY, type PopoverContextValue } from './context'\n\n  const ctx = getContext<PopoverContextValue>(POPOVER_CONTEXT_KEY)\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: PopoverAnchorProps = $props()\n\n  function registerAnchor(node: HTMLElement) {\n    ctx.registerAnchor(node)\n    return {\n      destroy: () => ctx.unregisterAnchor(node),\n    }\n  }\n</script>\n\n<!-- Positioning needs a measurable box, so unlike the headless reka anchor\n     this renders a shrink-wrap span. Content prefers it over the trigger. -->\n<span\n  use:registerAnchor\n  {...restProps}\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"popover-anchor\"\n  class={cn('inline-block', className)}\n>\n  {@render children?.()}\n</span>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/popover/PopoverAnchor.svelte"
    },
    {
      "path": "packages/registry-svelte/components/popover/PopoverContent.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type { PopoverAlign, PopoverSide } from './context'\n\n  export interface PopoverContentProps extends HTMLAttributes<HTMLDivElement> {\n    side?: PopoverSide\n    align?: PopoverAlign\n    sideOffset?: number\n    alignOffset?: number\n    onPointerDownOutside?: (e: PointerEvent) => void\n    onEscapeKeyDown?: (e: KeyboardEvent) => void\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { getContext } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { POPOVER_CONTEXT_KEY, type PopoverContextValue } from './context'\n\n  const ctx = getContext<PopoverContextValue>(POPOVER_CONTEXT_KEY)\n\n  let {\n    class: className,\n    side = 'bottom',\n    align = 'center',\n    sideOffset = 4,\n    alignOffset = 0,\n    onPointerDownOutside,\n    onEscapeKeyDown,\n    ref = $bindable(null),\n    children,\n    ...restProps\n  }: PopoverContentProps = $props()\n\n  const isOpen = $derived(ctx.isOpen())\n\n  let contentEl = $state<HTMLDivElement | null>(null)\n  // Hidden until first measured so the panel never flashes at 0,0.\n  let positioned = $state(false)\n\n  function portal(node: HTMLElement) {\n    document.body.appendChild(node)\n    return {\n      // Svelte also detaches the node on {#if} teardown; remove() on a\n      // detached node is a no-op, so double-removal is safe.\n      destroy: () => node.remove(),\n    }\n  }\n\n  function capture(node: HTMLDivElement) {\n    contentEl = node\n    return {\n      destroy: () => {\n        contentEl = null\n      },\n    }\n  }\n\n  // Origin sits on the edge facing the trigger so zoom/fade animations grow\n  // out of the anchor (parity with reka's transform-origin behavior).\n  const transformOrigin = $derived.by(() => {\n    const along = align === 'start' ? '0%' : align === 'end' ? '100%' : '50%'\n    if (side === 'bottom') return `${along} 0%`\n    if (side === 'top') return `${along} 100%`\n    if (side === 'right') return `0% ${along}`\n    return `100% ${along}`\n  })\n\n  const VIEWPORT_MARGIN = 8\n\n  function updatePosition() {\n    const el = contentEl\n    const anchor = ctx.getAnchorEl() ?? ctx.getTriggerEl()\n    if (!el || !anchor) return\n    const r = anchor.getBoundingClientRect()\n    const w = el.offsetWidth\n    const h = el.offsetHeight\n\n    let top: number\n    let left: number\n    if (side === 'top') {\n      top = r.top - h - sideOffset\n      left = align === 'start' ? r.left + alignOffset : align === 'end' ? r.right - w - alignOffset : r.left + r.width / 2 - w / 2 + alignOffset\n    } else if (side === 'bottom') {\n      top = r.bottom + sideOffset\n      left = align === 'start' ? r.left + alignOffset : align === 'end' ? r.right - w - alignOffset : r.left + r.width / 2 - w / 2 + alignOffset\n    } else if (side === 'left') {\n      left = r.left - w - sideOffset\n      top = align === 'start' ? r.top + alignOffset : align === 'end' ? r.bottom - h - alignOffset : r.top + r.height / 2 - h / 2 + alignOffset\n    } else {\n      left = r.right + sideOffset\n      top = align === 'start' ? r.top + alignOffset : align === 'end' ? r.bottom - h - alignOffset : r.top + r.height / 2 - h / 2 + alignOffset\n    }\n\n    // Clamp into the viewport (hand-rolled substitute for collision flipping).\n    left = Math.min(Math.max(VIEWPORT_MARGIN, left), Math.max(VIEWPORT_MARGIN, window.innerWidth - w - VIEWPORT_MARGIN))\n    top = Math.min(Math.max(VIEWPORT_MARGIN, top), Math.max(VIEWPORT_MARGIN, window.innerHeight - h - VIEWPORT_MARGIN))\n\n    el.style.top = `${top}px`\n    el.style.left = `${left}px`\n    positioned = true\n  }\n\n  function onDocumentPointerDown(e: PointerEvent) {\n    const el = contentEl\n    const target = e.target as Node | null\n    if (!el || !target) return\n    if (el.contains(target)) return\n    if (ctx.getTriggerEl()?.contains(target)) return\n    onPointerDownOutside?.(e)\n    if (e.defaultPrevented) return\n    // Parity with the Vue twin: esc/manual/none ignore outside pointer.\n    const mode = ctx.getCloseBehavior()\n    if (mode === 'esc' || mode === 'manual' || mode === 'none') return\n    ctx.setOpen(false)\n  }\n\n  function trapTab(e: KeyboardEvent) {\n    const el = contentEl\n    if (!el) return\n    const focusables = el.querySelectorAll<HTMLElement>(\n      'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n    )\n    if (focusables.length === 0) {\n      e.preventDefault()\n      return\n    }\n    const first = focusables[0]!\n    const last = focusables[focusables.length - 1]!\n    if (e.shiftKey && document.activeElement === first) {\n      e.preventDefault()\n      last.focus()\n    } else if (!e.shiftKey && document.activeElement === last) {\n      e.preventDefault()\n      first.focus()\n    }\n  }\n\n  function onDocumentKeyDown(e: KeyboardEvent) {\n    if (e.key === 'Escape') {\n      onEscapeKeyDown?.(e)\n      if (e.defaultPrevented) return\n      // Parity with the Vue twin: click-outside/manual/none suppress Escape.\n      const mode = ctx.getCloseBehavior()\n      if (mode === 'click-outside' || mode === 'manual' || mode === 'none') return\n      ctx.setOpen(false)\n      return\n    }\n    if (e.key === 'Tab' && ctx.isModal()) trapTab(e)\n  }\n\n  $effect(() => {\n    if (!isOpen) return\n    // Reads side/align/offsets (tracked) so repositioning follows prop changes.\n    updatePosition()\n    const raf = requestAnimationFrame(() => updatePosition())\n    const onScroll = () => updatePosition()\n    window.addEventListener('scroll', onScroll, { capture: true, passive: true })\n    window.addEventListener('resize', onScroll)\n    document.addEventListener('pointerdown', onDocumentPointerDown, true)\n    document.addEventListener('keydown', onDocumentKeyDown, true)\n    // Move focus into the panel on open (reka autofocus parity).\n    contentEl?.focus({ preventScroll: true })\n    return () => {\n      cancelAnimationFrame(raf)\n      window.removeEventListener('scroll', onScroll, { capture: true })\n      window.removeEventListener('resize', onScroll)\n      document.removeEventListener('pointerdown', onDocumentPointerDown, true)\n      document.removeEventListener('keydown', onDocumentKeyDown, true)\n    }\n  })\n\n  // Return focus to the trigger on close (reka parity).\n  let wasOpen = false\n  $effect(() => {\n    if (wasOpen && !isOpen) ctx.getTriggerEl()?.focus({ preventScroll: true })\n    wasOpen = isOpen\n  })\n</script>\n\n{#if isOpen}\n  <div\n    use:portal\n    use:capture\n    {...restProps}\n    bind:this={ref}\n    data-uipkge=\"\"\n    data-slot=\"popover-content\"\n    data-state=\"open\"\n    data-side={side}\n    data-align={align}\n    id={ctx.contentId}\n    role=\"dialog\"\n    tabindex={-1}\n    class={cn(\n      'bg-popover text-popover-foreground motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:ease-emphasized motion-safe:data-[state=open]:blur-in-2 motion-safe:data-[state=closed]:blur-out-2 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--uipkge-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden motion-safe:data-[state=closed]:duration-[var(--dur-exit)] motion-safe:data-[state=open]:duration-200',\n      className,\n    )}\n    style:position=\"fixed\"\n    style:top=\"0\"\n    style:left=\"0\"\n    style:visibility={positioned ? 'visible' : 'hidden'}\n    style:--uipkge-popover-content-transform-origin={transformOrigin}\n  >\n    {@render children?.()}\n  </div>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/popover/PopoverContent.svelte"
    },
    {
      "path": "packages/registry-svelte/components/popover/PopoverTrigger.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLButtonAttributes } from 'svelte/elements'\n\n  export interface PopoverTriggerProps extends HTMLButtonAttributes {\n    /** Render your own element with the trigger's props instead of emitting a\n     *  <button> — the Svelte counterpart of React's `asChild`. Spread `props`\n     *  onto your element. Positioning anchors to the wrapping span. */\n    child?: Snippet<[{ props: Record<string, unknown> }]>\n    ref?: HTMLButtonElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { getContext } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { POPOVER_CONTEXT_KEY, type PopoverContextValue } from './context'\n\n  const ctx = getContext<PopoverContextValue>(POPOVER_CONTEXT_KEY)\n\n  let {\n    class: className,\n    type = 'button',\n    child,\n    children,\n    ref = $bindable(null),\n    onclick,\n    ...restProps\n  }: PopoverTriggerProps = $props()\n\n  const isOpen = $derived(ctx.isOpen())\n\n  // In child mode the node is the `contents` wrapper span, which has no box and\n  // can't take focus — register the consumer's element (its first child)\n  // instead, so positioning, outside-click and focus return use the real\n  // trigger, as with React's asChild.\n  function registerTrigger(node: HTMLElement) {\n    const el = (child ? (node.firstElementChild as HTMLElement | null) : null) ?? node\n    ctx.registerTrigger(el)\n    return {\n      destroy: () => ctx.unregisterTrigger(el),\n    }\n  }\n\n  function handleClick(e: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {\n    onclick?.(e)\n    if (e.defaultPrevented) return\n    ctx.toggle()\n  }\n\n  const mergedProps = $derived({\n    'data-uipkge': '',\n    'data-slot': 'popover-trigger',\n    id: ctx.triggerId,\n    'aria-haspopup': 'dialog' as const,\n    'aria-expanded': isOpen,\n    'aria-controls': ctx.contentId,\n    'data-state': isOpen ? 'open' : 'closed',\n    class: cn(className),\n    onclick: handleClick,\n    ...restProps,\n  })\n</script>\n\n{#if child}\n  <!-- `contents` keeps this wrapper out of layout, so the consumer's element\n       sits in its parent's flex/grid exactly like React's asChild. -->\n  <span use:registerTrigger data-uipkge=\"\" data-slot=\"popover-trigger\" class=\"contents\">\n    {@render child({ props: mergedProps })}\n  </span>\n{:else}\n  <button use:registerTrigger bind:this={ref} {type} {...mergedProps}>{@render children?.()}</button>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/popover/PopoverTrigger.svelte"
    },
    {
      "path": "packages/registry-svelte/components/popover/context.ts",
      "content": "export type PopoverCloseBehavior = 'auto' | 'click-outside' | 'esc' | 'manual' | 'none'\n\nexport type PopoverSide = 'top' | 'right' | 'bottom' | 'left'\n\nexport type PopoverAlign = 'start' | 'center' | 'end'\n\nexport interface PopoverContextValue {\n  id: string\n  contentId: string\n  triggerId: string\n  isOpen: () => boolean\n  setOpen: (open: boolean) => void\n  toggle: () => void\n  isModal: () => boolean\n  getCloseBehavior: () => PopoverCloseBehavior\n  getTriggerEl: () => HTMLElement | null\n  getAnchorEl: () => HTMLElement | null\n  registerTrigger: (el: HTMLElement) => void\n  unregisterTrigger: (el: HTMLElement) => void\n  registerAnchor: (el: HTMLElement) => void\n  unregisterAnchor: (el: HTMLElement) => void\n}\n\nexport const POPOVER_CONTEXT_KEY = Symbol('uipkge-popover')\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/popover/context.ts"
    },
    {
      "path": "packages/registry-svelte/components/popover/index.ts",
      "content": "export { default as Popover, type PopoverProps } from './Popover.svelte'\nexport { default as PopoverAnchor, type PopoverAnchorProps } from './PopoverAnchor.svelte'\nexport { default as PopoverContent, type PopoverContentProps } from './PopoverContent.svelte'\nexport { default as PopoverTrigger, type PopoverTriggerProps } from './PopoverTrigger.svelte'\nexport type { PopoverCloseBehavior, PopoverSide, PopoverAlign } from './context'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/popover/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Click-triggered floating panel anchored to a trigger element. Supports optional localStorage persistence and configurable dismissal (click-outside, escape, manual). Hand-rolled runes positioning with viewport clamping.",
  "categories": [
    "overlay"
  ]
}