{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "tooltip",
  "title": "Tooltip",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/tooltip/Tooltip.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n\n  export interface TooltipProps {\n    /** Controlled open state. Bind it for two-way updates. */\n    open?: boolean\n    /** Initial open state for uncontrolled use. */\n    defaultOpen?: boolean\n    /** Called whenever the open state changes. */\n    onOpenChange?: (open: boolean) => void\n    /** Extra classes for the positioning wrapper (e.g. `w-full` when the\n     *  trigger child is full-width, like a sidebar menu button). */\n    class?: string\n    children?: Snippet\n  }\n</script>\n\n<script lang=\"ts\">\n  import { untrack } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { TooltipRootState, getTooltipProviderState, setTooltipRootState } from './context.svelte'\n\n  let { defaultOpen = false, open = $bindable(defaultOpen), onOpenChange, class: className, children }: TooltipProps = $props()\n\n  const root = new TooltipRootState()\n  root.provider = getTooltipProviderState()\n  root.onOpenChange = (next) => {\n    open = next\n    onOpenChange?.(next)\n  }\n  setTooltipRootState(root)\n\n  // Controlled mode: reflect the bound value. Untracked so internal\n  // trigger/content updates (which already write through to `open`) don't\n  // ping-pong back into the root.\n  $effect(() => {\n    const next = open\n    untrack(() => {\n      if (next !== root.open) root.setOpen(next)\n    })\n  })\n</script>\n\n<!-- Relative anchor for the absolutely-positioned content (no portal: the\n     hand-rolled content renders inline and positions against this box). -->\n<span data-uipkge=\"\" data-slot=\"tooltip\" data-state={root.open ? 'open' : 'closed'} class={cn('relative inline-block', className)}>\n  {@render children?.()}\n</span>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/tooltip/Tooltip.svelte"
    },
    {
      "path": "packages/registry-svelte/components/tooltip/TooltipContent.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface TooltipContentProps extends HTMLAttributes<HTMLDivElement> {\n    side?: 'top' | 'right' | 'bottom' | 'left'\n    sideOffset?: number\n    align?: 'start' | 'center' | 'end'\n    alignOffset?: number\n    /** Show the arrow pointer. Default true. */\n    showArrow?: boolean\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getTooltipRootState } from './context.svelte'\n\n  let {\n    class: className,\n    side = 'top',\n    sideOffset = 4,\n    align = 'center',\n    alignOffset = 0,\n    showArrow = true,\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: TooltipContentProps = $props()\n\n  const root = getTooltipRootState()\n\n  const positionClass = $derived.by(() => {\n    switch (side) {\n      case 'top':\n        return align === 'start'\n          ? 'bottom-full left-0'\n          : align === 'end'\n            ? 'bottom-full right-0'\n            : 'bottom-full left-1/2 -translate-x-1/2'\n      case 'bottom':\n        return align === 'start'\n          ? 'top-full left-0'\n          : align === 'end'\n            ? 'top-full right-0'\n            : 'top-full left-1/2 -translate-x-1/2'\n      case 'left':\n        return align === 'start'\n          ? 'right-full top-0'\n          : align === 'end'\n            ? 'right-full bottom-0'\n            : 'right-full top-1/2 -translate-y-1/2'\n      case 'right':\n        return align === 'start'\n          ? 'left-full top-0'\n          : align === 'end'\n            ? 'left-full bottom-0'\n            : 'left-full top-1/2 -translate-y-1/2'\n    }\n  })\n\n  const arrowClass = $derived.by(() => {\n    switch (side) {\n      case 'top':\n        return 'bottom-[-3px] left-1/2 -translate-x-1/2'\n      case 'bottom':\n        return 'top-[-3px] left-1/2 -translate-x-1/2'\n      case 'left':\n        return 'right-[-3px] top-1/2 -translate-y-1/2'\n      case 'right':\n        return 'left-[-3px] top-1/2 -translate-y-1/2'\n    }\n  })\n\n  const offsetStyle = $derived.by(() => {\n    const main = `${sideOffset}px`\n    const cross = alignOffset ? `${alignOffset}px` : undefined\n    const style: Record<string, string> = {}\n    if (side === 'top') style['margin-bottom'] = main\n    else if (side === 'bottom') style['margin-top'] = main\n    else if (side === 'left') style['margin-right'] = main\n    else style['margin-left'] = main\n    if (cross) {\n      if (side === 'top' || side === 'bottom') style['margin-left'] = cross\n      else style['margin-top'] = cross\n    }\n    return Object.entries(style)\n      .map(([k, v]) => `${k}: ${v}`)\n      .join('; ')\n  })\n</script>\n\n{#if root?.open}\n  <div\n    bind:this={ref}\n    id={root.id}\n    role=\"tooltip\"\n    data-uipkge=\"\"\n    data-slot=\"tooltip-content\"\n    data-side={side}\n    data-align={align}\n    data-state=\"open\"\n    class={cn(\n      'bg-foreground text-background motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:data-[state=closed]:animate-out motion-safe:blur-in-2 motion-safe:data-[state=closed]:blur-out-2 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=closed]:zoom-out-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 absolute z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance',\n      positionClass,\n      className,\n    )}\n    style={offsetStyle}\n    {...restProps}\n  >\n    {@render children?.()}\n    {#if showArrow}\n      <div\n        data-slot=\"tooltip-arrow\"\n        aria-hidden=\"true\"\n        class={cn(\n          'bg-foreground fill-foreground absolute z-50 size-2.5 rotate-45 rounded-[2px]',\n          arrowClass,\n        )}\n      ></div>\n    {/if}\n  </div>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/tooltip/TooltipContent.svelte"
    },
    {
      "path": "packages/registry-svelte/components/tooltip/TooltipProvider.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n\n  export interface TooltipProviderProps {\n    /** ms before a hovered tooltip opens. Default 700. */\n    delayDuration?: number\n    /** Window after a close during which the next tooltip skips the delay. Default 300. */\n    skipDelayDuration?: number\n    children?: Snippet\n  }\n</script>\n\n<script lang=\"ts\">\n  import { TooltipProviderState, setTooltipProviderState } from './context.svelte'\n\n  let { delayDuration = 700, skipDelayDuration = 300, children }: TooltipProviderProps = $props()\n\n  const state = new TooltipProviderState()\n  setTooltipProviderState(state)\n\n  $effect(() => {\n    state.delayDuration = delayDuration\n    state.skipDelayDuration = skipDelayDuration\n  })\n</script>\n\n{@render children?.()}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/tooltip/TooltipProvider.svelte"
    },
    {
      "path": "packages/registry-svelte/components/tooltip/TooltipTrigger.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  type SpanAttrs = HTMLAttributes<HTMLSpanElement>\n  type TriggerPointerEvent = Parameters<NonNullable<SpanAttrs['onpointerenter']>>[0]\n  type TriggerFocusEvent = Parameters<NonNullable<SpanAttrs['onfocus']>>[0]\n  type TriggerKeydownEvent = Parameters<NonNullable<SpanAttrs['onkeydown']>>[0]\n\n  export interface TooltipTriggerProps extends HTMLAttributes<HTMLSpanElement> {\n    /** Render your own element with the trigger handlers instead of the\n     *  default wrapping span — the Svelte counterpart of `asChild`. */\n    child?: Snippet<[{ props: Record<string, unknown> }]>\n    ref?: HTMLSpanElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getTooltipRootState } from './context.svelte'\n\n  let {\n    class: className,\n    child,\n    children,\n    ref = $bindable(null),\n    onpointerenter,\n    onpointerleave,\n    onfocus,\n    onblur,\n    onkeydown,\n    ...restProps\n  }: TooltipTriggerProps = $props()\n\n  const root = getTooltipRootState()\n  const describedBy = $derived(root?.open ? root.id : undefined)\n\n  function handlePointerEnter(event: TriggerPointerEvent) {\n    onpointerenter?.(event)\n    if (!event.defaultPrevented) root?.scheduleOpen()\n  }\n\n  function handlePointerLeave(event: TriggerPointerEvent) {\n    onpointerleave?.(event)\n    if (!event.defaultPrevented) root?.cancelScheduledOpen()\n  }\n\n  function handleFocus(event: TriggerFocusEvent) {\n    onfocus?.(event)\n    if (!event.defaultPrevented) root?.setOpen(true)\n  }\n\n  function handleBlur(event: TriggerFocusEvent) {\n    onblur?.(event)\n    if (!event.defaultPrevented) root?.setOpen(false)\n  }\n\n  function handleKeydown(event: TriggerKeydownEvent) {\n    onkeydown?.(event)\n    if (event.key === 'Escape' && root?.open) root.setOpen(false)\n  }\n\n  const mergedProps = $derived({\n    'data-uipkge': '',\n    'data-slot': 'tooltip-trigger',\n    'data-state': root?.open ? 'open' : 'closed',\n    'aria-describedby': describedBy,\n    class: cn('inline-block', className),\n    onpointerenter: handlePointerEnter,\n    onpointerleave: handlePointerLeave,\n    onfocus: handleFocus,\n    onblur: handleBlur,\n    onkeydown: handleKeydown,\n    ...restProps,\n  })\n</script>\n\n{#if child}\n  {@render child({ props: mergedProps })}\n{:else}\n  <!-- The wrapper span lets tooltips fire on disabled controls inside it.\n       Positioning anchors to the Tooltip root, not this span. -->\n  <span bind:this={ref} {...mergedProps}>\n    {@render children?.()}\n  </span>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/tooltip/TooltipTrigger.svelte"
    },
    {
      "path": "packages/registry-svelte/components/tooltip/context.svelte.ts",
      "content": "import { getContext, setContext } from 'svelte'\n\nlet tooltipCounter = 0\n\nexport class TooltipProviderState {\n  delayDuration = $state(700)\n  skipDelayDuration = $state(300)\n  /** Timestamp of the last tooltip close, for skip-delay behavior. */\n  lastCloseAt = $state(0)\n}\n\nconst TOOLTIP_PROVIDER_KEY = Symbol('TooltipProvider')\n\nexport function setTooltipProviderState(state: TooltipProviderState) {\n  setContext(TOOLTIP_PROVIDER_KEY, state)\n}\n\nexport function getTooltipProviderState(): TooltipProviderState | undefined {\n  return getContext<TooltipProviderState | undefined>(TOOLTIP_PROVIDER_KEY)\n}\n\nexport class TooltipRootState {\n  id = `tooltip-${++tooltipCounter}`\n  open = $state(false)\n  provider: TooltipProviderState | undefined = undefined\n  onOpenChange: ((open: boolean) => void) | undefined = undefined\n  private openTimer: ReturnType<typeof setTimeout> | null = null\n\n  setOpen(next: boolean) {\n    this.clearTimer()\n    if (this.open === next) return\n    if (!next && this.provider) this.provider.lastCloseAt = Date.now()\n    this.open = next\n    this.onOpenChange?.(next)\n  }\n\n  /** Hover entry: honor the provider delay unless another tooltip just closed. */\n  scheduleOpen() {\n    this.clearTimer()\n    if (this.open) return\n    const delay = this.provider?.delayDuration ?? 700\n    const skip = this.provider?.skipDelayDuration ?? 300\n    if (delay <= 0 || Date.now() - (this.provider?.lastCloseAt ?? 0) < skip) {\n      this.setOpen(true)\n      return\n    }\n    this.openTimer = setTimeout(() => this.setOpen(true), delay)\n  }\n\n  cancelScheduledOpen() {\n    this.clearTimer()\n    this.setOpen(false)\n  }\n\n  clearTimer() {\n    if (this.openTimer) {\n      clearTimeout(this.openTimer)\n      this.openTimer = null\n    }\n  }\n}\n\nconst TOOLTIP_ROOT_KEY = Symbol('TooltipRoot')\n\nexport function setTooltipRootState(state: TooltipRootState) {\n  setContext(TOOLTIP_ROOT_KEY, state)\n}\n\nexport function getTooltipRootState(): TooltipRootState | undefined {\n  return getContext<TooltipRootState | undefined>(TOOLTIP_ROOT_KEY)\n}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/tooltip/context.svelte.ts"
    },
    {
      "path": "packages/registry-svelte/components/tooltip/index.ts",
      "content": "export { default as Tooltip, type TooltipProps } from './Tooltip.svelte'\nexport { default as TooltipContent, type TooltipContentProps } from './TooltipContent.svelte'\nexport { default as TooltipProvider, type TooltipProviderProps } from './TooltipProvider.svelte'\nexport { default as TooltipTrigger, type TooltipTriggerProps } from './TooltipTrigger.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/tooltip/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Small popover triggered by hover/focus, used for short labels — icon-button names, abbreviation expansions, keyboard shortcuts. Auto-positions and respects `prefers-reduced-motion`.",
  "categories": [
    "overlay"
  ]
}