{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dialog",
  "title": "Dialog",
  "description": "The question, set large, with two plain answers. The page holds still behind it.",
  "dependencies": [
    "@radix-ui/react-slot"
  ],
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/scrollbar.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/dialog.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\nimport { Scrollbar } from \"@/components/ui/scrollbar\"\n\ntype DialogContextValue = {\n  open: boolean\n  setOpen: (open: boolean) => void\n  alert: boolean\n  titleId: string\n  descriptionId: string\n}\n\nconst DialogContext = React.createContext<DialogContextValue | null>(null)\n\nfunction useDialog() {\n  const ctx = React.useContext(DialogContext)\n  if (!ctx) throw new Error(\"Dialog parts must sit inside <Dialog>.\")\n  return ctx\n}\n\ntype DialogProps = {\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  /** A question that needs an answer: role alertdialog, and pointing outside doesn't close it. */\n  alert?: boolean\n  children: React.ReactNode\n}\n\n/** The question, set large. Built on the native <dialog>, so focus, Escape and the top layer are the browser's. */\nfunction Dialog({ open: controlled, defaultOpen = false, onOpenChange, alert = false, children }: DialogProps) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultOpen)\n  const open = controlled ?? uncontrolled\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (controlled === undefined) setUncontrolled(next)\n      onOpenChange?.(next)\n    },\n    [controlled, onOpenChange],\n  )\n  const id = React.useId()\n  const value = React.useMemo(\n    () => ({ open, setOpen, alert, titleId: `${id}-title`, descriptionId: `${id}-description` }),\n    [open, setOpen, alert, id],\n  )\n  return <DialogContext.Provider value={value}>{children}</DialogContext.Provider>\n}\n\nfunction DialogTrigger({ asChild = false, onClick, ...props }: React.ComponentProps<\"button\"> & { asChild?: boolean }) {\n  const { open, setOpen } = useDialog()\n  const Comp = asChild ? Slot : \"button\"\n  return (\n    <Comp\n      data-slot=\"dialog-trigger\"\n      aria-haspopup=\"dialog\"\n      aria-expanded={open}\n      onClick={(e: React.MouseEvent<HTMLButtonElement>) => {\n        onClick?.(e)\n        if (!e.defaultPrevented) setOpen(true)\n      }}\n      {...(asChild ? {} : { type: \"button\" as const })}\n      {...props}\n    />\n  )\n}\n\n/**\n * The bare <dialog>, opened as a modal while the Dialog is open. The sheet and\n * the drawer are surfaces too; they bring their own class.\n * Focus goes to the element marked data-autofocus, or the browser's first choice.\n * Taller than the window, it scrolls, on the scrollbar's rail.\n */\nfunction DialogSurface({ className, children, onClick, ref: forwardedRef, ...props }: React.ComponentProps<\"dialog\">) {\n  const { open, setOpen, alert, titleId, descriptionId } = useDialog()\n  const ref = React.useRef<HTMLDialogElement>(null)\n  const composedRef = useComposedRefs(ref, forwardedRef)\n\n  React.useEffect(() => {\n    const d = ref.current\n    if (!d) return\n    d.setAttribute(\"closedby\", alert ? \"closerequest\" : \"any\")\n    if (open && !d.open) {\n      d.showModal()\n      d.querySelector<HTMLElement>(\"[data-autofocus]\")?.focus()\n    } else if (!open && d.open) d.close()\n  }, [open, alert])\n\n  React.useEffect(() => {\n    const d = ref.current\n    if (!d) return\n    const closed = () => setOpen(false)\n    d.addEventListener(\"close\", closed)\n    return () => d.removeEventListener(\"close\", closed)\n  }, [setOpen])\n\n  return (\n    <dialog\n      ref={composedRef}\n      role={alert ? \"alertdialog\" : undefined}\n      aria-labelledby={titleId}\n      aria-describedby={descriptionId}\n      className={className}\n      onClick={(e) => {\n        onClick?.(e)\n        // Pointing outside puts it away (where the browser lacks closedby=\"any\").\n        const d = ref.current\n        if (alert || !d || e.target !== d) return\n        const r = d.getBoundingClientRect()\n        if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) d.close()\n      }}\n      {...props}\n    >\n      {children}\n      <Scrollbar />\n    </dialog>\n  )\n}\n\ntype DialogContentProps = React.ComponentProps<\"dialog\"> & {\n  /**\n   * frame: corner marks and a row of facts over the question. reply: the question heavy and narrow, the answers\n   * set large in the italic, because the answer is yours; use bare DialogClose buttons for them. ruled: a poster's\n   * grid of hairlines, the question large on ruled lines, the detail on the far side, the answers in the cells below.\n   */\n  variant?: \"frame\" | \"reply\" | \"ruled\"\n}\n\nfunction DialogContent({ className, variant = \"frame\", ...props }: DialogContentProps) {\n  return (\n    <DialogSurface\n      data-slot=\"dialog-content\"\n      data-variant={variant === \"frame\" ? undefined : variant}\n      className={cn(\"ot-dialog\", className)}\n      {...props}\n    />\n  )\n}\n\n/** A frame row over the question: what this is, a hairline, and one fact. */\nfunction DialogMeta({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"dialog-meta\" className={cn(\"ot-meta ot-dialog-meta\", className)} {...props} />\n}\n\nfunction DialogTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n  const { titleId } = useDialog()\n  return <h2 data-slot=\"dialog-title\" id={titleId} className={cn(\"ot-dialog-title\", className)} {...props} />\n}\n\nfunction DialogDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n  const { descriptionId } = useDialog()\n  return <p data-slot=\"dialog-description\" id={descriptionId} className={cn(\"ot-dialog-body\", className)} {...props} />\n}\n\nfunction DialogActions({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"dialog-actions\" className={cn(\"ot-dialog-actions\", className)} {...props} />\n}\n\nfunction DialogClose({ asChild = false, onClick, ...props }: React.ComponentProps<\"button\"> & { asChild?: boolean }) {\n  const { setOpen } = useDialog()\n  const Comp = asChild ? Slot : \"button\"\n  return (\n    <Comp\n      data-slot=\"dialog-close\"\n      onClick={(e: React.MouseEvent<HTMLButtonElement>) => {\n        onClick?.(e)\n        if (!e.defaultPrevented) setOpen(false)\n      }}\n      {...(asChild ? {} : { type: \"button\" as const })}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Dialog,\n  DialogTrigger,\n  DialogSurface,\n  DialogContent,\n  DialogMeta,\n  DialogTitle,\n  DialogDescription,\n  DialogActions,\n  DialogActions as DialogFooter,\n  DialogClose,\n  useDialog,\n  type DialogContentProps,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/dialog.css",
      "content": "@layer components {\n  /* ── ot-dialog ── the question, set large */\n  .ot-dialog {\n    border: 0; margin: auto; padding: var(--ot-space-8) var(--ot-space-7) var(--ot-space-7);\n    width: min(44rem, calc(100vw - 2 * var(--ot-margin))); max-width: none;\n    background: var(--ot-paper); color: var(--ot-graphite);\n    --ot-corner: 14px;\n    opacity: 0; translate: 0 12px;\n    transition: opacity var(--ot-andante) var(--ot-exhale), translate var(--ot-andante) var(--ot-exhale), overlay var(--ot-andante) allow-discrete, display var(--ot-andante) allow-discrete;\n  }\n  .ot-dialog[open] { opacity: 1; translate: 0 0; }\n  @starting-style { .ot-dialog[open] { opacity: 0; translate: 0 12px; } }\n  :is(.ot-dialog, .ot-sheet, .ot-drawer)::backdrop {\n    background: color-mix(in srgb, var(--ot-paper) 86%, transparent);\n    -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);\n    opacity: 0;\n    transition: opacity var(--ot-andante), overlay var(--ot-andante) allow-discrete, display var(--ot-andante) allow-discrete;\n  }\n  :is(.ot-dialog, .ot-sheet, .ot-drawer)[open]::backdrop { opacity: 1; }\n  @starting-style { :is(.ot-dialog, .ot-sheet, .ot-drawer)[open]::backdrop { opacity: 0; } }\n  .ot-dialog-meta { margin-block-end: var(--ot-space-7); }\n  .ot-dialog[open]::after { animation: ot-frame var(--ot-andante) var(--ot-exhale) 80ms backwards; }\n  .ot-dialog[open] .ot-dialog-meta > hr { transform-origin: 0 50%; animation: ot-draw-x var(--ot-andante) var(--ot-exhale) 200ms backwards; }\n  .ot-dialog[open] .ot-dialog-title { animation: ot-breathe-out var(--ot-andante) var(--ot-exhale) 120ms backwards; }\n  @keyframes ot-frame { from { opacity: 0; scale: 0.95; } } /* opens out from inside, so the dialog never scrolls */\n  @keyframes ot-breathe-out { from { opacity: 0; font-weight: 500; font-stretch: 88%; } }\n  .ot-dialog-title .ot-yours { margin-inline-start: 0.06em; } /* Bodoni's italic leans into the word before it at this size */\n  .ot-dialog-title { font-size: var(--ot-f); line-height: var(--ot-f-lh); letter-spacing: var(--ot-f-tr); font-weight: 300; color: var(--ot-ink); }\n  .ot-dialog-body { margin-block-start: var(--ot-space-4); max-inline-size: 40ch; }\n  .ot-dialog-actions { margin: var(--ot-space-7) 0 0; display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--ot-space-6); }\n  @media (max-width: 40rem) { .ot-dialog { padding: var(--ot-space-7) var(--ot-space-5) var(--ot-space-6); } .ot-dialog-meta { margin-block-end: var(--ot-space-6); } .ot-dialog-actions { margin-block-start: var(--ot-space-6); gap: var(--ot-space-5); } }\n\n  /* Reply (\"It has to be design.\"): the question is ours, heavy and narrow; the answers are yours, set large in the\n     italic. The one you point at, or that holds focus, takes a full stop in the accent, and only one ever does. */\n  .ot-dialog[data-variant][open] .ot-dialog-title { animation: none; }\n  .ot-dialog[data-variant=\"reply\"] .ot-dialog-title { font-weight: 800; font-stretch: 72%; line-height: 0.98; letter-spacing: -0.015em; word-spacing: 0.06em; text-wrap: balance; }\n  .ot-dialog[data-variant=\"reply\"] .ot-dialog-actions { justify-content: flex-start; align-items: baseline; column-gap: var(--ot-space-7); row-gap: var(--ot-space-4); }\n  .ot-dialog[data-variant=\"reply\"] .ot-dialog-actions > button:not(.ot-btn) {\n    appearance: none; border: 0; padding: 0; background: none; cursor: pointer;\n    font-family: var(--ot-expression); font-style: italic; font-weight: 400; font-size: calc(var(--ot-mf) * var(--ot-expression-scale)); line-height: 1; letter-spacing: 0;\n    color: var(--ot-graphite); transition: color var(--ot-allegro) var(--ot-breath);\n  }\n  .ot-dialog[data-variant=\"reply\"] .ot-dialog-actions > button:not(.ot-btn)::after { content: \".\" / \"\"; display: inline-block; color: var(--ot-accent); scale: 0; transform-origin: 50% 85%; transition: scale var(--ot-moderato) var(--ot-spiccato); }\n  .ot-dialog[data-variant=\"reply\"] .ot-dialog-actions > button:not(.ot-btn):is(:hover, :focus, [data-force~=\"hover\"]) { color: var(--ot-ink); }\n  .ot-dialog[data-variant=\"reply\"] .ot-dialog-actions > button:not(.ot-btn):is(:hover, :focus, [data-force~=\"hover\"])::after { scale: 1; }\n  .ot-dialog[data-variant=\"reply\"] .ot-dialog-actions:has(> :hover) > button:not(.ot-btn, :hover) { color: var(--ot-graphite); }\n  .ot-dialog[data-variant=\"reply\"] .ot-dialog-actions:has(> :hover) > button:not(.ot-btn, :hover)::after { scale: 0; }\n\n  /* Ruled (\"Less is more.\"): a grid of hairlines to the dialog's edges. Facts in the top corners; the question large,\n     each of its lines standing on a rule that draws across as it opens; the detail ragged on the far side; the\n     answers in the cells below. */\n  .ot-dialog[data-variant=\"ruled\"] { --pad: var(--ot-space-6); width: min(58rem, calc(100vw - 2 * var(--ot-margin))); padding: 0; }\n  .ot-dialog[data-variant=\"ruled\"][open] { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 16rem); }\n  .ot-dialog[data-variant=\"ruled\"]::before { content: \"\"; grid-column: 2; grid-row: 2 / 4; border-inline-start: var(--ot-hairline) solid var(--ot-ink); pointer-events: none; }\n  .ot-dialog[data-variant=\"ruled\"] .ot-dialog-meta { grid-column: 1 / -1; grid-row: 1; margin: 0; padding: var(--ot-space-4) var(--pad); justify-content: space-between; border-block-end: var(--ot-hairline) solid var(--ot-ink); }\n  .ot-dialog[data-variant=\"ruled\"] .ot-dialog-meta > hr { display: none; }\n  .ot-dialog[data-variant=\"ruled\"] .ot-dialog-title { grid-column: 1; grid-row: 2; position: relative; isolation: isolate; padding: var(--ot-space-8) var(--pad) 0; font-size: var(--ot-ff); line-height: 1.15; letter-spacing: var(--ot-ff-tr); font-weight: 500; }\n  .ot-dialog[data-variant=\"ruled\"] .ot-dialog-title::after { content: \"\"; position: absolute; z-index: -1; inset: var(--ot-space-8) 0 -1px; background: repeating-linear-gradient(to bottom, transparent 0 calc(1lh - 1px), var(--ot-ink) calc(1lh - 1px) 1lh); transform-origin: 0 50%; }\n  .ot-dialog[data-variant=\"ruled\"] .ot-dialog-title .ot-yours { line-height: 0; } /* the italic mustn't grow a line, or the rules fall out of step */\n  :is([dir=\"rtl\"], [dir=\"rtl\"] *).ot-dialog[data-variant=\"ruled\"] .ot-dialog-title::after { transform-origin: 100% 50%; }\n  .ot-dialog[data-variant=\"ruled\"][open] .ot-dialog-title::after { animation: ot-draw-x var(--ot-andante) var(--ot-exhale) 160ms backwards; }\n  .ot-dialog[data-variant=\"ruled\"] .ot-dialog-body { grid-column: 2; grid-row: 2; align-self: center; margin: 0; padding: var(--ot-space-5); max-inline-size: none; text-align: end; text-wrap: pretty; }\n  .ot-dialog[data-variant=\"ruled\"] .ot-dialog-actions { grid-column: 1 / -1; grid-row: 3; margin: 0; display: grid; grid-template-columns: subgrid; align-items: center; gap: 0; border-block-start: var(--ot-hairline) solid var(--ot-ink); }\n  .ot-dialog[data-variant=\"ruled\"] .ot-dialog-actions > * { grid-column: 1; justify-self: start; margin: var(--ot-space-5) var(--pad); }\n  .ot-dialog[data-variant=\"ruled\"] .ot-dialog-actions > :last-child:not(:first-child) { grid-column: 2; justify-self: end; margin-inline: var(--ot-space-5); }\n  @media (max-width: 40rem) {\n    .ot-dialog[data-variant=\"ruled\"] { --pad: var(--ot-space-5); }\n    .ot-dialog[data-variant=\"ruled\"][open] { grid-template-columns: minmax(0, 1fr); }\n    .ot-dialog[data-variant=\"ruled\"]::before { content: none; }\n    .ot-dialog[data-variant=\"ruled\"] .ot-dialog-title { grid-row: auto; padding-block: var(--ot-space-7) var(--ot-space-5); font-size: var(--ot-f); }\n    .ot-dialog[data-variant=\"ruled\"] .ot-dialog-title::after { inset-block: var(--ot-space-7) var(--ot-space-5); }\n    .ot-dialog[data-variant=\"ruled\"] .ot-dialog-body { grid-column: 1; grid-row: auto; padding: 0 var(--pad) var(--ot-space-6); text-align: start; }\n    .ot-dialog[data-variant=\"ruled\"] .ot-dialog-actions { grid-column: 1; grid-row: auto; display: flex; justify-content: space-between; }\n    .ot-dialog[data-variant=\"ruled\"] .ot-dialog-actions > :last-child:not(:first-child) { margin-inline: var(--pad); }\n  }\n  @media (forced-colors: active) {\n    .ot-dialog[data-variant=\"ruled\"] .ot-dialog-title::after { forced-color-adjust: none; background: repeating-linear-gradient(to bottom, transparent 0 calc(1lh - 1px), CanvasText calc(1lh - 1px) 1lh); }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/dialog.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/dialog.css\"": {}
  },
  "meta": {
    "movement": "VII",
    "underneath": "hook",
    "contract": "ot-dialog"
  },
  "type": "registry:ui"
}
