{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "dialog",
  "title": "Dialog",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/dialog/Dialog.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n\n  export interface DialogProps {\n    /** Controlled open state. The initial value doubles as `defaultOpen` —\n     *  pass `open` once for an uncontrolled dialog with a custom default. */\n    open?: boolean\n    /** When false the dialog is non-modal: content skips the focus trap,\n     *  body scroll-lock, and autofocus, and the overlay stops intercepting\n     *  pointer events so the page stays interactive (Radix `modal` parity). */\n    modal?: boolean\n    onOpenChange?: (open: boolean) => void\n    children?: Snippet\n  }\n\n  let dialogIdCounter = 0\n</script>\n\n<script lang=\"ts\">\n  import { setDialogContext } from './context'\n\n  let { open = $bindable(false), modal = true, onOpenChange, children }: DialogProps = $props()\n\n  dialogIdCounter += 1\n  const titleId = `uipkge-dialog-title-${dialogIdCounter}`\n  const descriptionId = `uipkge-dialog-description-${dialogIdCounter}`\n\n  function setOpen(next: boolean) {\n    if (next === open) return\n    open = next\n    onOpenChange?.(next)\n  }\n\n  setDialogContext({\n    get open() {\n      return open\n    },\n    setOpen,\n    get titleId() {\n      return titleId\n    },\n    get descriptionId() {\n      return descriptionId\n    },\n    get modal() {\n      return modal\n    },\n  })\n</script>\n\n{@render children?.()}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/Dialog.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dialog/DialogClose.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLButtonAttributes } from 'svelte/elements'\n\n  export interface DialogCloseProps extends HTMLButtonAttributes {\n    /** Render your own element as the close control instead of a <button>. */\n    child?: Snippet<[{ props: Record<string, unknown> }]>\n    ref?: HTMLButtonElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { getDialogContext } from './context'\n\n  type OnClick = NonNullable<HTMLButtonAttributes['onclick']>\n\n  let {\n    class: className,\n    type = 'button',\n    child,\n    children,\n    ref = $bindable(null),\n    onclick,\n    ...restProps\n  }: DialogCloseProps = $props()\n\n  const ctx = getDialogContext()\n\n  const closeDialog: OnClick = (e) => {\n    onclick?.(e)\n    if (e.defaultPrevented) return\n    ctx.setOpen(false)\n  }\n\n  const mergedProps = $derived({\n    'data-uipkge': '',\n    'data-slot': 'dialog-close',\n    class: className,\n    type,\n    onclick: closeDialog,\n    ...restProps,\n  })\n</script>\n\n{#if child}\n  {@render child({ props: mergedProps })}\n{:else}\n  <button bind:this={ref} {...mergedProps}>{@render children?.()}</button>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/DialogClose.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dialog/DialogContent.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface DialogContentProps extends HTMLAttributes<HTMLDivElement> {\n    showCloseButton?: boolean\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { tick } from 'svelte'\n  import { X } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  import { getDialogContext } from './context'\n  import { portal } from './portal'\n  import DialogClose from './DialogClose.svelte'\n  import DialogOverlay from './DialogOverlay.svelte'\n\n  let {\n    class: className,\n    showCloseButton = true,\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: DialogContentProps = $props()\n\n  const ctx = getDialogContext()\n\n  $effect(() => {\n    if (!ctx.open) return\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        e.preventDefault()\n        ctx.setOpen(false)\n        return\n      }\n      // Minimal focus trap: cycle Tab within the panel.\n      if (e.key !== 'Tab' || !ref) return\n      const focusables = ref.querySelectorAll<HTMLElement>(\n        'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex=\"-1\"])',\n      )\n      if (!focusables.length) return\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    window.addEventListener('keydown', onKeyDown)\n    const prevOverflow = document.body.style.overflow\n    document.body.style.overflow = 'hidden'\n    tick().then(() => ref?.focus({ preventScroll: true }))\n    return () => {\n      window.removeEventListener('keydown', onKeyDown)\n      document.body.style.overflow = prevOverflow\n    }\n  })\n</script>\n\n{#if ctx.open}\n  <div use:portal>\n    <DialogOverlay />\n    <div\n      bind:this={ref}\n      data-uipkge\n      data-slot=\"dialog-content\"\n      role=\"dialog\"\n      aria-modal=\"true\"\n      aria-labelledby={ctx.titleId}\n      aria-describedby={ctx.descriptionId}\n      tabindex=\"-1\"\n      data-state=\"open\"\n      class={cn(\n        'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:ease-emphasized data-[state=open]:blur-in-2 data-[state=closed]:blur-out-2 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 data-[state=closed]:duration-[var(--dur-exit)] data-[state=open]:duration-200 sm:max-w-lg',\n        className,\n      )}\n      {...restProps}\n    >\n      {@render children?.()}\n\n      {#if showCloseButton}\n        <DialogClose\n          class=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n        >\n          <X aria-hidden=\"true\" />\n          <span class=\"sr-only\">Close</span>\n        </DialogClose>\n      {/if}\n    </div>\n  </div>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/DialogContent.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dialog/DialogDescription.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface DialogDescriptionProps extends HTMLAttributes<HTMLParagraphElement> {\n    ref?: HTMLParagraphElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getDialogContextOrNull } from './context'\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: DialogDescriptionProps = $props()\n\n  const ctx = getDialogContextOrNull()\n</script>\n\n<p\n  bind:this={ref}\n  data-uipkge\n  data-slot=\"dialog-description\"\n  id={ctx?.descriptionId}\n  class={cn('text-muted-foreground text-sm', className)}\n  {...restProps}\n>\n  {@render children?.()}\n</p>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/DialogDescription.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dialog/DialogFooter.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface DialogFooterProps extends HTMLAttributes<HTMLDivElement> {\n    showCloseButton?: boolean\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Button } from '$lib/components/ui/button'\n  import { cn } from '$lib/utils'\n  import DialogClose from './DialogClose.svelte'\n\n  let {\n    class: className,\n    showCloseButton = false,\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: DialogFooterProps = $props()\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge\n  data-slot=\"dialog-footer\"\n  class={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}\n  {...restProps}\n>\n  {@render children?.()}\n  {#if showCloseButton}\n    <DialogClose>\n      {#snippet child({ props })}\n        <Button variant=\"outline\" {...props}>Close</Button>\n      {/snippet}\n    </DialogClose>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/DialogFooter.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dialog/DialogHeader.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface DialogHeaderProps extends HTMLAttributes<HTMLDivElement> {\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: DialogHeaderProps = $props()\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge\n  data-slot=\"dialog-header\"\n  class={cn('flex flex-col gap-2 text-center sm:text-left', className)}\n  {...restProps}\n>\n  {@render children?.()}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/DialogHeader.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dialog/DialogOverlay.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface DialogOverlayProps extends HTMLAttributes<HTMLDivElement> {\n    /** Fired on pointer-down outside the panel, before close. Call\n     *  `preventDefault()` to keep the dialog open (Radix parity). */\n    onPointerDownOutside?: (e: PointerEvent) => void\n    /** Override the `data-slot` value (e.g. `command-dialog-overlay`). Default `'dialog-overlay'`. */\n    dataSlot?: string\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getDialogContext } from './context'\n  import { portal } from './portal'\n\n  type OnClick = NonNullable<HTMLAttributes<HTMLDivElement>['onclick']>\n  type OnPointerDown = NonNullable<HTMLAttributes<HTMLDivElement>['onpointerdown']>\n\n  let {\n    class: className,\n    children,\n    dataSlot = 'dialog-overlay',\n    ref = $bindable(null),\n    onclick,\n    onpointerdown,\n    onPointerDownOutside,\n    ...restProps\n  }: DialogOverlayProps = $props()\n\n  const ctx = getDialogContext()\n\n  // Set by the pointerdown that precedes a backdrop click; when the outside\n  // handler prevents default, the matching click must not close.\n  let outsidePrevented = false\n\n  const firePointerDownOutside: OnPointerDown = (e) => {\n    onpointerdown?.(e)\n    onPointerDownOutside?.(e)\n    outsidePrevented = e.defaultPrevented\n  }\n\n  const closeOnBackdrop: OnClick = (e) => {\n    onclick?.(e)\n    if (e.defaultPrevented || outsidePrevented) {\n      outsidePrevented = false\n      return\n    }\n    outsidePrevented = false\n    ctx.setOpen(false)\n  }\n</script>\n\n{#if ctx.open}\n  <div\n    bind:this={ref}\n    use:portal\n    data-uipkge\n    data-slot={dataSlot}\n    data-state=\"open\"\n    class={cn(\n      'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:ease-emphasized data-[state=open]:blur-in-2 data-[state=closed]:blur-out-2 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-foreground/50 fixed inset-0 z-50 data-[state=closed]:duration-[var(--dur-exit)] data-[state=open]:duration-200',\n      !ctx.modal && 'pointer-events-none',\n      className,\n    )}\n    {...restProps}\n    onclick={closeOnBackdrop}\n    onpointerdown={firePointerDownOutside}\n  >\n    {@render children?.()}\n  </div>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/DialogOverlay.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dialog/DialogScrollContent.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface DialogScrollContentProps extends HTMLAttributes<HTMLDivElement> {\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { tick } from 'svelte'\n  import { X } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  import { getDialogContext } from './context'\n  import { portal } from './portal'\n  import DialogClose from './DialogClose.svelte'\n\n  type OnClick = NonNullable<HTMLAttributes<HTMLDivElement>['onclick']>\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: DialogScrollContentProps = $props()\n\n  const ctx = getDialogContext()\n\n  function handleBackdropClick(e: MouseEvent) {\n    // Only backdrop clicks close — clicks inside the panel bubble with a\n    // different target. Scrollbar clicks land on the container itself, so\n    // exclude them like the Vue twin does.\n    if (e.target !== e.currentTarget) return\n    const el = e.currentTarget as HTMLElement\n    if (e.offsetX > el.clientWidth || e.offsetY > el.clientHeight) return\n    ctx.setOpen(false)\n  }\n\n  const onBackdropClick: OnClick = (e) => handleBackdropClick(e)\n\n  $effect(() => {\n    if (!ctx.open) return\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        e.preventDefault()\n        ctx.setOpen(false)\n        return\n      }\n      if (e.key !== 'Tab' || !ref) return\n      const focusables = ref.querySelectorAll<HTMLElement>(\n        'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex=\"-1\"])',\n      )\n      if (!focusables.length) return\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    window.addEventListener('keydown', onKeyDown)\n    const prevOverflow = document.body.style.overflow\n    document.body.style.overflow = 'hidden'\n    tick().then(() => ref?.focus({ preventScroll: true }))\n    return () => {\n      window.removeEventListener('keydown', onKeyDown)\n      document.body.style.overflow = prevOverflow\n    }\n  })\n</script>\n\n{#if ctx.open}\n  <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions: backdrop click-catcher; keyboard users dismiss with Escape -->\n  <div\n    use:portal\n    data-uipkge\n    data-slot=\"dialog-overlay\"\n    data-state=\"open\"\n    class=\"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:ease-emphasized data-[state=open]:blur-in-2 data-[state=closed]:blur-out-2 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-foreground/50 fixed inset-0 z-50 grid place-items-center overflow-y-auto data-[state=closed]:duration-[var(--dur-exit)] data-[state=open]:duration-200\"\n    onclick={onBackdropClick}\n  >\n    <div\n      bind:this={ref}\n      data-uipkge\n      data-slot=\"dialog-content\"\n      role=\"dialog\"\n      aria-modal=\"true\"\n      aria-labelledby={ctx.titleId}\n      aria-describedby={ctx.descriptionId}\n      tabindex=\"-1\"\n      class={cn(\n        'bg-background relative z-50 my-8 grid w-full max-w-lg gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',\n        className,\n      )}\n      {...restProps}\n    >\n      {@render children?.()}\n\n      <DialogClose class=\"hover:bg-secondary absolute top-4 right-4 rounded-md p-0.5 transition-colors duration-200\">\n        <X class=\"h-4 w-4\" aria-hidden=\"true\" />\n        <span class=\"sr-only\">Close</span>\n      </DialogClose>\n    </div>\n  </div>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/DialogScrollContent.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dialog/DialogTitle.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface DialogTitleProps extends HTMLAttributes<HTMLHeadingElement> {\n    ref?: HTMLHeadingElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getDialogContextOrNull } from './context'\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: DialogTitleProps = $props()\n\n  const ctx = getDialogContextOrNull()\n</script>\n\n<h2\n  bind:this={ref}\n  data-uipkge\n  data-slot=\"dialog-title\"\n  id={ctx?.titleId}\n  class={cn('text-lg leading-none font-semibold', className)}\n  {...restProps}\n>\n  {@render children?.()}\n</h2>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/DialogTitle.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dialog/DialogTrigger.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLButtonAttributes } from 'svelte/elements'\n\n  export interface DialogTriggerProps extends HTMLButtonAttributes {\n    /** Render your own element as the trigger instead of a <button>. */\n    child?: Snippet<[{ props: Record<string, unknown> }]>\n    ref?: HTMLButtonElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { getDialogContext } from './context'\n\n  type OnClick = NonNullable<HTMLButtonAttributes['onclick']>\n\n  let {\n    class: className,\n    type = 'button',\n    child,\n    children,\n    ref = $bindable(null),\n    onclick,\n    ...restProps\n  }: DialogTriggerProps = $props()\n\n  const ctx = getDialogContext()\n\n  const openDialog: OnClick = (e) => {\n    onclick?.(e)\n    if (e.defaultPrevented) return\n    ctx.setOpen(true)\n  }\n\n  const mergedProps = $derived({\n    'data-uipkge': '',\n    'data-slot': 'dialog-trigger',\n    class: className,\n    type,\n    onclick: openDialog,\n    ...restProps,\n  })\n</script>\n\n{#if child}\n  {@render child({ props: mergedProps })}\n{:else}\n  <button bind:this={ref} {...mergedProps}>{@render children?.()}</button>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/DialogTrigger.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dialog/context.ts",
      "content": "import { getContext, setContext } from 'svelte'\n\nexport interface DialogContextValue {\n  readonly open: boolean\n  setOpen: (open: boolean) => void\n  readonly titleId: string\n  readonly descriptionId: string\n  /** False renders non-modal: no focus trap, body scroll-lock, or autofocus\n   *  (Radix `modal` parity). The overlay still renders unless the consumer\n   *  hides it — like Radix, non-modal panels typically omit the overlay. */\n  readonly modal: boolean\n}\n\nconst DIALOG_CONTEXT_KEY = Symbol('uipkge-dialog')\n\nexport function setDialogContext(value: DialogContextValue): void {\n  setContext(DIALOG_CONTEXT_KEY, value)\n}\n\nexport function getDialogContext(): DialogContextValue {\n  const value = getContext<DialogContextValue | undefined>(DIALOG_CONTEXT_KEY)\n  if (!value) throw new Error('Dialog parts must be used inside <Dialog>')\n  return value\n}\n\nexport function getDialogContextOrNull(): DialogContextValue | null {\n  return getContext<DialogContextValue | null>(DIALOG_CONTEXT_KEY) ?? null\n}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/context.ts"
    },
    {
      "path": "packages/registry-svelte/components/dialog/portal.ts",
      "content": "import type { Action } from 'svelte/action'\n\n/**\n * Move the node to `document.body` (or a selector/element target) while it is\n * mounted. Actions only run in the browser, so this is SSR-safe.\n */\nexport const portal: Action<HTMLElement, HTMLElement | string | undefined> = (node, target) => {\n  const targetEl = typeof target === 'string' ? document.querySelector(target) : (target ?? document.body)\n  ;(targetEl ?? document.body).appendChild(node)\n  return {\n    destroy() {\n      node.remove()\n    },\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/portal.ts"
    },
    {
      "path": "packages/registry-svelte/components/dialog/index.ts",
      "content": "export { default as Dialog, type DialogProps } from './Dialog.svelte'\nexport { default as DialogClose, type DialogCloseProps } from './DialogClose.svelte'\nexport { default as DialogContent, type DialogContentProps } from './DialogContent.svelte'\nexport { default as DialogDescription, type DialogDescriptionProps } from './DialogDescription.svelte'\nexport { default as DialogFooter, type DialogFooterProps } from './DialogFooter.svelte'\nexport { default as DialogHeader, type DialogHeaderProps } from './DialogHeader.svelte'\nexport { default as DialogOverlay, type DialogOverlayProps } from './DialogOverlay.svelte'\nexport { default as DialogScrollContent, type DialogScrollContentProps } from './DialogScrollContent.svelte'\nexport { default as DialogTitle, type DialogTitleProps } from './DialogTitle.svelte'\nexport { default as DialogTrigger, type DialogTriggerProps } from './DialogTrigger.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dialog/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/button.json"
  ],
  "description": "Free-form modal primitive — composable from `Dialog`, `DialogTrigger`, `DialogContent`, and friends. Use for forms, info cards, pickers, and any custom modal layout. Hand-rolled runes behavior: portal, Escape/outside close, scroll lock, focus trap, and labelledby wiring.",
  "categories": [
    "overlay"
  ]
}