{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "release-history-dialog",
  "title": "Release History Dialog",
  "author": "STARCK",
  "description": "A responsive, accessible release-notes dialog.",
  "dependencies": [
    "framer-motion@^11.0.0"
  ],
  "files": [
    {
      "path": "registry/default/release-history-dialog.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { createPortal } from \"react-dom\"\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\"\n\ntype ReleaseEntry = {\n  version: string\n  date: string\n  notes: readonly string[]\n}\n\ntype ReleaseHistoryDialogProps = {\n  releases: readonly ReleaseEntry[]\n  triggerLabel?: string\n  title?: string\n  description?: string\n}\n\nfunction ReleaseHistoryDialog({\n  releases,\n  triggerLabel = \"Release history\",\n  title = \"Release notes\",\n  description = \"Every release, newest first.\",\n}: ReleaseHistoryDialogProps) {\n  const [open, setOpen] = React.useState(false)\n  const reduceMotion = useReducedMotion()\n  const titleId = React.useId()\n  const triggerRef = React.useRef<HTMLButtonElement>(null)\n  const dialogRef = React.useRef<HTMLDivElement>(null)\n  const closeRef = React.useRef<HTMLButtonElement>(null)\n\n  React.useEffect(() => {\n    if (!open) return\n    const handleKey = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") setOpen(false)\n      if (event.key !== \"Tab\") return\n\n      const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(\n        'button:not([disabled]):not([tabindex=\"-1\"]), a[href], [tabindex]:not([tabindex=\"-1\"])'\n      )\n      if (!focusable?.length) return\n      const first = focusable[0]\n      const last = focusable[focusable.length - 1]\n      if (event.shiftKey && document.activeElement === first) {\n        event.preventDefault()\n        last.focus()\n      } else if (!event.shiftKey && document.activeElement === last) {\n        event.preventDefault()\n        first.focus()\n      }\n    }\n    const previous = document.body.style.overflow\n    const returnFocus = triggerRef.current\n    document.body.style.overflow = \"hidden\"\n    const frame = requestAnimationFrame(() => closeRef.current?.focus())\n    window.addEventListener(\"keydown\", handleKey)\n    return () => {\n      cancelAnimationFrame(frame)\n      document.body.style.overflow = previous\n      window.removeEventListener(\"keydown\", handleKey)\n      returnFocus?.focus()\n    }\n  }, [open])\n\n  return (\n    <>\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        onClick={() => setOpen(true)}\n        className=\"cursor-pointer rounded-full border border-black/10 bg-white px-4 py-2 text-sm font-medium text-zinc-900 shadow-sm hover:bg-zinc-50\"\n      >\n        {triggerLabel}\n      </button>\n      {typeof document !== \"undefined\"\n        ? createPortal(\n            <AnimatePresence>\n              {open ? (\n                <motion.div\n                  role=\"dialog\"\n                  aria-modal=\"true\"\n                  aria-labelledby={titleId}\n                  className=\"fixed inset-0 z-[100] flex items-end justify-center sm:items-center sm:p-6\"\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  exit={{ opacity: 0 }}\n                  transition={{ duration: reduceMotion ? 0 : 0.18 }}\n                >\n                  <button\n                    type=\"button\"\n                    tabIndex={-1}\n                    aria-label=\"Close release history\"\n                    onClick={() => setOpen(false)}\n                    className=\"absolute inset-0 cursor-default bg-black/40 backdrop-blur-sm\"\n                  />\n                  <motion.div\n                    ref={dialogRef}\n                    className=\"relative flex max-h-[88vh] w-full max-w-2xl flex-col overflow-hidden rounded-t-3xl border border-black/10 bg-white shadow-2xl sm:max-h-[80vh] sm:rounded-3xl\"\n                    initial={\n                      reduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 0, y: 24, scale: 0.985 }\n                    }\n                    animate={{ opacity: 1, y: 0, scale: 1 }}\n                    exit={\n                      reduceMotion\n                        ? { opacity: 0 }\n                        : { opacity: 0, y: 14, scale: 0.99 }\n                    }\n                    transition={\n                      reduceMotion\n                        ? { duration: 0 }\n                        : { duration: 0.26, ease: [0.22, 1, 0.36, 1] }\n                    }\n                  >\n                    <header className=\"flex shrink-0 items-start justify-between gap-4 border-b border-black/8 px-5 py-4\">\n                      <div>\n                        <h2\n                          id={titleId}\n                          className=\"font-semibold tracking-tight text-zinc-950\"\n                        >\n                          {title}\n                        </h2>\n                        <p className=\"mt-1 text-xs text-zinc-500\">\n                          {description}\n                        </p>\n                      </div>\n                      <button\n                        ref={closeRef}\n                        type=\"button\"\n                        onClick={() => setOpen(false)}\n                        aria-label=\"Close\"\n                        className=\"grid size-8 cursor-pointer place-items-center rounded-full text-zinc-500 hover:bg-zinc-100 hover:text-zinc-950\"\n                      >\n                        ×\n                      </button>\n                    </header>\n                    <ol className=\"space-y-4 overflow-y-auto p-5\">\n                      {releases.map((release) => (\n                        <li\n                          key={release.version}\n                          className=\"rounded-2xl border border-black/8 bg-zinc-50/70 p-5\"\n                        >\n                          <div className=\"flex flex-wrap items-center gap-2\">\n                            <h3 className=\"font-semibold text-zinc-950\">\n                              Version {release.version}\n                            </h3>\n                            <time className=\"rounded-full bg-white px-2.5 py-1 text-[11px] text-zinc-500 ring-1 ring-black/6\">\n                              {release.date}\n                            </time>\n                          </div>\n                          <ul className=\"mt-3 space-y-2 text-sm leading-6 text-zinc-600\">\n                            {release.notes.map((note) => (\n                              <li key={note} className=\"flex gap-2\">\n                                <span\n                                  aria-hidden\n                                  className=\"mt-2 size-1 shrink-0 rounded-full bg-zinc-400\"\n                                />\n                                <span>{note}</span>\n                              </li>\n                            ))}\n                          </ul>\n                        </li>\n                      ))}\n                    </ol>\n                  </motion.div>\n                </motion.div>\n              ) : null}\n            </AnimatePresence>,\n            document.body\n          )\n        : null}\n    </>\n  )\n}\n\nexport { ReleaseHistoryDialog }\nexport type { ReleaseEntry, ReleaseHistoryDialogProps }\n",
      "type": "registry:component",
      "target": "components/release-history-dialog.tsx"
    }
  ],
  "type": "registry:component"
}