{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cuts",
  "title": "Cuts",
  "description": "Film cuts for shadcn. Add <Cuts /> once to your root layout.",
  "files": [
    {
      "path": "components/keyframery/cuts.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useLayoutEffect } from \"react\"\n\nimport { applyMenus, setEnabled, setPace, setResponses, start, type Menus, type Responses } from \"@/lib/keyframery/engine\"\n\nimport \"./keyframery.css\"\n\nexport type CutsProps = Menus & {\n  /** Duration multiplier for the whole app. Beats --kf-pace in your CSS. */\n  pace?: number\n  /** false = stock shadcn motion everywhere. */\n  enabled?: boolean\n  /** \"none\" keeps the cuts but turns off how controls answer a press, a tick or a drag. */\n  responses?: Responses\n}\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect\n\n/** Film cuts for shadcn. Render once, in your root layout. */\nexport function Cuts({ dialog, sheet, drawer, tabs, toast, pace, enabled = true, responses }: CutsProps) {\n  useIsoLayoutEffect(() => {\n    applyMenus({ dialog, sheet, drawer, tabs, toast })\n  }, [dialog, sheet, drawer, tabs, toast])\n  useIsoLayoutEffect(() => {\n    setEnabled(enabled)\n  }, [enabled])\n  useIsoLayoutEffect(() => {\n    setPace(pace)\n  }, [pace])\n  useIsoLayoutEffect(() => {\n    setResponses(responses)\n  }, [responses])\n  useEffect(() => start(), [])\n  return null\n}\n",
      "type": "registry:component",
      "target": "components/keyframery/cuts.tsx"
    },
    {
      "path": "components/keyframery/keyframery.css",
      "content": "/*\n * Keyframery v0: film cuts for shadcn. https://keyframery.com\n *\n * - Unlayered on purpose: it beats Tailwind's @layer utilities, where shadcn's animate-in classes live.\n * - Everything is scoped under html[data-kf], which <Cuts /> sets while enabled.\n * - The menu choice lives on <html> (data-kf-dialog=\"punch-in\"); an element's own data-cut beats it.\n *   The two selector forms exclude each other, so specificity never decides.\n * - \"Open\" matches Radix (data-state) and Base UI (data-open); \"closed\" likewise.\n * - Entrances fill backwards only: once in, the part is back to its own styles (a held transform or\n *   filter, even at zero, would make it a containing block for its fixed-position children).\n *   Exits fill both, so the part stays invisible until the library removes it.\n */\n\n:where(:root) {\n  --kf-pace: 1;\n  --kf-ease: cubic-bezier(0.22, 1, 0.36, 1);\n  --kf-ease-exit: cubic-bezier(0.5, 0, 0.75, 0);\n  --kf-travel: 0.35;\n  --kf-blur: 8px;\n  --kf-depth: 0.94;\n  --kf-hold: 300ms;\n  --kf-cap: 1000s;\n}\n\n\n/* ---------- Dialog, alert dialog, command: rack focus ---------- */\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"rack-focus\"]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"])[data-cut=\"rack-focus\"]\n  ):is([data-state=\"open\"], [data-open]:not([data-open=\"false\"])) {\n  animation: kf-rack-in min(calc(340ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"rack-focus\"]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"])[data-cut=\"rack-focus\"]\n  ):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation: kf-rack-out min(calc(220ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) both;\n}\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"rack-focus\"]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"])[data-cut=\"rack-focus\"]\n  ):is([data-state=\"open\"], [data-open]:not([data-open=\"false\"])) {\n  animation: kf-veil-in min(calc(340ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"rack-focus\"]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"])[data-cut=\"rack-focus\"]\n  ):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation: kf-veil-out min(calc(220ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) both;\n}\n\n@keyframes kf-rack-in {\n  from {\n    opacity: 0;\n    transform: translate(calc(var(--kf-dx, 0px) * var(--kf-travel)), calc(var(--kf-dy, 0px) * var(--kf-travel))) scale(var(--kf-depth));\n    filter: blur(var(--kf-blur));\n  }\n  to {\n    opacity: 1;\n    transform: none;\n    filter: blur(0);\n  }\n}\n\n@keyframes kf-rack-out {\n  from {\n    opacity: 1;\n    transform: none;\n    filter: blur(0);\n  }\n  to {\n    opacity: 0;\n    transform: translate(calc(var(--kf-dx, 0px) * var(--kf-travel)), calc(var(--kf-dy, 0px) * var(--kf-travel))) scale(var(--kf-depth));\n    filter: blur(calc(var(--kf-blur) * 0.75));\n  }\n}\n\n@keyframes kf-veil-in {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes kf-veil-out {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n  }\n}\n\n\n/* ---------- Sheet: travels in from its own edge ---------- */\n\n/* Base UI sheets use CSS transitions, which would beat our keyframes; turn them off wherever we animate. */\n:is(\n    :where(html[data-kf]:not([data-kf-sheet=\"none\"])) :is([data-slot=\"sheet-content\"], [data-slot=\"sheet-overlay\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"sheet-content\"], [data-slot=\"sheet-overlay\"])[data-cut]:not([data-cut=\"none\"])\n  ) {\n  transition: none;\n}\n\n:where(html[data-kf]) [data-slot=\"sheet-content\"] {\n  --kf-sx: 100%;\n  --kf-sy: 0px;\n}\n:where(html[data-kf]) [data-slot=\"sheet-content\"][data-side=\"left\"] {\n  --kf-sx: -100%;\n}\n:where(html[data-kf]) [data-slot=\"sheet-content\"][data-side=\"top\"] {\n  --kf-sx: 0px;\n  --kf-sy: -100%;\n}\n:where(html[data-kf]) [data-slot=\"sheet-content\"][data-side=\"bottom\"] {\n  --kf-sx: 0px;\n  --kf-sy: 100%;\n}\n\n:is(\n    :where(html[data-kf]:is([data-kf-sheet=\"slide-sink\"], [data-kf-sheet=\"slide\"])) [data-slot=\"sheet-content\"]:not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) [data-slot=\"sheet-content\"]:is([data-cut=\"slide-sink\"], [data-cut=\"slide\"])\n  ):is([data-state=\"open\"], [data-open]:not([data-open=\"false\"])) {\n  animation: kf-panel-in min(calc(460ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:is(\n    :where(html[data-kf]:is([data-kf-sheet=\"slide-sink\"], [data-kf-sheet=\"slide\"])) [data-slot=\"sheet-content\"]:not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) [data-slot=\"sheet-content\"]:is([data-cut=\"slide-sink\"], [data-cut=\"slide\"])\n  ):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation: kf-panel-out min(calc(260ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) both;\n}\n\n:is(\n    :where(html[data-kf]:is([data-kf-sheet=\"slide-sink\"], [data-kf-sheet=\"slide\"])) [data-slot=\"sheet-overlay\"]:not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) [data-slot=\"sheet-overlay\"]:is([data-cut=\"slide-sink\"], [data-cut=\"slide\"])\n  ):is([data-state=\"open\"], [data-open]:not([data-open=\"false\"])) {\n  animation: kf-veil-in min(calc(460ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:is(\n    :where(html[data-kf]:is([data-kf-sheet=\"slide-sink\"], [data-kf-sheet=\"slide\"])) [data-slot=\"sheet-overlay\"]:not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) [data-slot=\"sheet-overlay\"]:is([data-cut=\"slide-sink\"], [data-cut=\"slide\"])\n  ):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation: kf-veil-out min(calc(260ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) both;\n}\n\n/* translate: none cancels Base UI's starting/ending-style translate so the two don't stack */\n@keyframes kf-panel-in {\n  from {\n    transform: translate(var(--kf-sx), var(--kf-sy));\n    translate: none;\n    opacity: 1;\n  }\n  to {\n    transform: none;\n    translate: none;\n    opacity: 1;\n  }\n}\n\n@keyframes kf-panel-out {\n  from {\n    transform: none;\n    translate: none;\n    opacity: 1;\n  }\n  to {\n    transform: translate(var(--kf-sx), var(--kf-sy));\n    translate: none;\n    opacity: 1;\n  }\n}\n\n/* ---------- Sink: the page steps back while a sheet or drawer is up ---------- */\n\n:where(html[data-kf]) [data-kf-sink] {\n  transform-origin: 50% 0;\n  transition:\n    scale min(calc(460ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease),\n    filter min(calc(460ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease),\n    border-radius min(calc(460ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease);\n}\n\n:where(html[data-kf]) [data-kf-sink=\"off\"] {\n  transition-timing-function: var(--kf-ease-exit);\n}\n\n:where(html[data-kf]) [data-kf-sink=\"on\"] {\n  scale: 0.975;\n  filter: brightness(0.94);\n  border-radius: 14px;\n  overflow: hidden;\n}\n\n:where(html[data-kf][data-kf-sinking]) body {\n  background-color: color-mix(in oklab, var(--background) 80%, black);\n}\n\n\n/* ---------- Dialog menu: punch-in and fade ---------- */\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"punch-in\"]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"])[data-cut=\"punch-in\"]\n  ):is([data-state=\"open\"], [data-open]:not([data-open=\"false\"])) {\n  animation: kf-punch-in min(calc(260ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"punch-in\"]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"])[data-cut=\"punch-in\"]\n  ):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation: kf-punch-out min(calc(160ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) both;\n}\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"fade\"]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"])[data-cut=\"fade\"]\n  ):is([data-state=\"open\"], [data-open]:not([data-open=\"false\"])) {\n  animation: kf-fade-in min(calc(200ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"fade\"]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-content\"], [data-slot=\"alert-dialog-content\"])[data-cut=\"fade\"]\n  ):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation: kf-fade-out min(calc(150ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) both;\n}\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"punch-in\"]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"])[data-cut=\"punch-in\"]\n  ):is([data-state=\"open\"], [data-open]:not([data-open=\"false\"])) {\n  animation: kf-veil-in min(calc(260ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"punch-in\"]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"])[data-cut=\"punch-in\"]\n  ):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation: kf-veil-out min(calc(160ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) both;\n}\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"fade\"]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"])[data-cut=\"fade\"]\n  ):is([data-state=\"open\"], [data-open]:not([data-open=\"false\"])) {\n  animation: kf-veil-in min(calc(200ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:is(\n    :where(html[data-kf][data-kf-dialog=\"fade\"]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"]):not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) :is([data-slot=\"dialog-overlay\"], [data-slot=\"alert-dialog-overlay\"])[data-cut=\"fade\"]\n  ):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation: kf-veil-out min(calc(150ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) both;\n}\n\n@keyframes kf-punch-in {\n  0% {\n    opacity: 0;\n    transform: scale(0.85);\n  }\n  70% {\n    opacity: 1;\n    transform: scale(1.02);\n  }\n  100% {\n    opacity: 1;\n    transform: none;\n  }\n}\n\n@keyframes kf-punch-out {\n  from {\n    opacity: 1;\n    transform: none;\n  }\n  to {\n    opacity: 0;\n    transform: scale(0.96);\n  }\n}\n\n@keyframes kf-fade-in {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes kf-fade-out {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n  }\n}\n\n/* ---------- Sheet menu: fade ---------- */\n\n:is(\n    :where(html[data-kf][data-kf-sheet=\"fade\"]) [data-slot=\"sheet-content\"]:not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) [data-slot=\"sheet-content\"][data-cut=\"fade\"]\n  ):is([data-state=\"open\"], [data-open]:not([data-open=\"false\"])) {\n  animation: kf-panel-fade-in min(calc(200ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:is(\n    :where(html[data-kf][data-kf-sheet=\"fade\"]) [data-slot=\"sheet-content\"]:not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) [data-slot=\"sheet-content\"][data-cut=\"fade\"]\n  ):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation: kf-panel-fade-out min(calc(150ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) both;\n}\n\n:is(\n    :where(html[data-kf][data-kf-sheet=\"fade\"]) [data-slot=\"sheet-overlay\"]:not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) [data-slot=\"sheet-overlay\"][data-cut=\"fade\"]\n  ):is([data-state=\"open\"], [data-open]:not([data-open=\"false\"])) {\n  animation: kf-veil-in min(calc(200ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:is(\n    :where(html[data-kf][data-kf-sheet=\"fade\"]) [data-slot=\"sheet-overlay\"]:not([data-cut], [data-cut=\"none\"] *),\n    :where(html[data-kf]) [data-slot=\"sheet-overlay\"][data-cut=\"fade\"]\n  ):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation: kf-veil-out min(calc(150ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) both;\n}\n\n@keyframes kf-panel-fade-in {\n  from {\n    opacity: 0;\n    transform: none;\n    translate: none;\n  }\n  to {\n    opacity: 1;\n    transform: none;\n    translate: none;\n  }\n}\n\n@keyframes kf-panel-fade-out {\n  from {\n    opacity: 1;\n    transform: none;\n    translate: none;\n  }\n  to {\n    opacity: 0;\n    transform: none;\n    translate: none;\n  }\n}\n\n\n/* ---------- Tuned: the libraries' own keyframes, retimed to the film's pace ---------- */\n\n:where(html[data-kf])\n  :is(\n    [data-slot=\"popover-content\"],\n    [data-slot=\"dropdown-menu-content\"],\n    [data-slot=\"dropdown-menu-sub-content\"],\n    [data-slot=\"context-menu-content\"],\n    [data-slot=\"context-menu-sub-content\"],\n    [data-slot=\"menubar-content\"],\n    [data-slot=\"menubar-sub-content\"],\n    [data-slot=\"select-content\"],\n    [data-slot=\"combobox-content\"],\n    [data-slot=\"hover-card-content\"],\n    [data-slot=\"tooltip-content\"],\n    [data-slot=\"navigation-menu-content\"],\n    [data-slot=\"navigation-menu-viewport\"],\n    [data-slot=\"accordion-content\"]\n  ):not([data-cut=\"none\"], [data-cut=\"none\"] *):is(\n    [data-state=\"open\"],\n    [data-state=\"delayed-open\"],\n    [data-state=\"instant-open\"],\n    [data-open]:not([data-open=\"false\"]),\n    [data-state=\"closed\"],\n    [data-closed]:not([data-closed=\"false\"])\n  ) {\n  animation-duration: min(calc(180ms * var(--kf-pace)), var(--kf-cap));\n  animation-timing-function: var(--kf-ease);\n  transition-duration: min(calc(180ms * var(--kf-pace)), var(--kf-cap));\n  transition-timing-function: var(--kf-ease);\n}\n\n:where(html[data-kf])\n  :is(\n    [data-slot=\"popover-content\"], [data-slot=\"dropdown-menu-content\"], [data-slot=\"dropdown-menu-sub-content\"],\n    [data-slot=\"context-menu-content\"], [data-slot=\"context-menu-sub-content\"], [data-slot=\"menubar-content\"],\n    [data-slot=\"menubar-sub-content\"], [data-slot=\"select-content\"], [data-slot=\"combobox-content\"],\n    [data-slot=\"hover-card-content\"], [data-slot=\"tooltip-content\"], [data-slot=\"navigation-menu-content\"],\n    [data-slot=\"navigation-menu-viewport\"], [data-slot=\"accordion-content\"]\n  ):not([data-cut=\"none\"], [data-cut=\"none\"] *):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n  animation-timing-function: var(--kf-ease-exit);\n  transition-timing-function: var(--kf-ease-exit);\n}\n\n/* Collapsible ships with no motion at all, which is a jump cut. It gets the accordion's height cut. */\n:where(html[data-kf]) [data-slot=\"collapsible-content\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *, [data-kf-static]):is(\n    [data-state=\"open\"],\n    [data-open]:not([data-open=\"false\"])\n  ) {\n  animation: kf-grow min(calc(180ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease);\n}\n\n:where(html[data-kf]) [data-slot=\"collapsible-content\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *):is(\n    [data-state=\"closed\"],\n    [data-closed]:not([data-closed=\"false\"])\n  ) {\n  animation: kf-shrink min(calc(180ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease-exit) forwards;\n}\n\n@keyframes kf-grow {\n  from {\n    height: 0;\n    overflow: clip;\n  }\n  to {\n    height: var(--radix-collapsible-content-height, var(--collapsible-panel-height, auto));\n    overflow: clip;\n  }\n}\n\n@keyframes kf-shrink {\n  from {\n    height: var(--radix-collapsible-content-height, var(--collapsible-panel-height, auto));\n    overflow: clip;\n  }\n  to {\n    height: 0;\n    overflow: clip;\n  }\n}\n\n\n/* ---------- Responses: how a control answers the user's own press, tick or drag ---------- */\n/*\n * Cuts animate a change of picture; responses confirm an input on the control itself. Timings follow\n * IBM Carbon's micro-interaction durations: 70 ms to press in, 110-150 ms for small state changes,\n * 240 ms for a value bar. <Cuts responses=\"none\" /> turns them all off; data-cut=\"none\" opts a part out.\n * Where stock shadcn already responds (the button's 1px press nudge, the slider thumb's ring, the switch\n * thumb's slide, the progress bar's width), these retime or complete it and never stack a second effect.\n */\n\n/* Toggle: the tick draws itself and the radio dot grows from its centre. The engine marks a control\n   (data-kf-toggled) only when its state flips, so controls that load already checked don't redraw. */\n:where(html[data-kf]:not([data-kf-responses=\"none\"]))\n  :is(\n    [data-slot=\"checkbox\"], [data-slot=\"questionnaire-choice\"], [data-slot=\"command-item\"], [data-slot=\"combobox-item\"],\n    [data-slot=\"dropdown-menu-checkbox-item\"], [data-slot=\"dropdown-menu-radio-item\"], [data-slot=\"context-menu-checkbox-item\"],\n    [data-slot=\"context-menu-radio-item\"], [data-slot=\"menubar-checkbox-item\"], [data-slot=\"menubar-radio-item\"]\n  )[data-kf-toggled=\"on\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *)\n  svg :is(path, polyline, line) {\n  stroke-dasharray: 30;\n  animation: kf-draw min(calc(150ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n:where(html[data-kf]:not([data-kf-responses=\"none\"]))\n  :is(\n    [data-slot=\"radio-group-item\"][data-kf-toggled=\"on\"] [data-slot=\"radio-group-indicator\"] > *,\n    [data-slot=\"questionnaire-choice\"][data-kf-toggled=\"on\"] [data-slot=\"questionnaire-choice-indicator-dot\"]\n  ):not([data-cut=\"none\"], [data-cut=\"none\"] *) {\n  animation: kf-dot-in min(calc(150ms * var(--kf-pace)), var(--kf-cap)) var(--kf-ease) backwards;\n}\n\n/* A choice card (a field label holding a checkbox or radio) changes colour instead of jumping. */\n:where(html[data-kf]:not([data-kf-responses=\"none\"])) [data-slot=\"field-label\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *) {\n  transition-property: color, background-color, border-color, box-shadow;\n  transition-duration: min(calc(150ms * var(--kf-pace)), var(--kf-cap));\n  transition-timing-function: var(--kf-ease);\n}\n\n@keyframes kf-draw {\n  from {\n    stroke-dashoffset: 30;\n  }\n  to {\n    stroke-dashoffset: 0;\n  }\n}\n\n@keyframes kf-dot-in {\n  from {\n    transform: scale(0); /* composes after the dot's own centring translate */\n  }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  /* Press: compact controls dip while held, quicker in than out. Full-width rows (sidebar items, menu\n     items) don't scale; they get Highlight instead. */\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    :is(\n      [data-slot=\"button\"], [data-slot=\"toggle\"], [data-slot=\"toggle-group-item\"], [data-slot=\"pagination-link\"],\n      [data-slot=\"carousel-previous\"], [data-slot=\"carousel-next\"], [data-slot=\"dialog-close\"], [data-slot=\"sheet-close\"],\n      [data-slot=\"alert-dialog-action\"], [data-slot=\"alert-dialog-cancel\"], [data-slot=\"attachment-action\"],\n      [data-slot=\"combobox-chip-remove\"], [data-slot=\"sidebar-trigger\"], [data-slot=\"message-scroller-button\"],\n      [data-slot=\"calendar\"] :is(.rdp-button_previous, .rdp-button_next)\n    ):not([data-cut=\"none\"], [data-cut=\"none\"] *):active {\n    scale: 0.97;\n    transition-duration: calc(70ms * var(--kf-pace));\n  }\n\n  /* State icons. The accordion's chevron turns instead of being swapped for an up chevron. An item that\n     loads open shows the turned chevron at once: an icon coming out of display:none has nothing to turn from. */\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"accordion-trigger\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *)\n    > [data-slot=\"accordion-trigger-icon\"]:has(+ [data-slot=\"accordion-trigger-icon\"]) {\n    display: block;\n    transition: rotate calc(200ms * var(--kf-pace)) var(--kf-ease);\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"accordion-trigger\"][aria-expanded=\"true\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *)\n    > [data-slot=\"accordion-trigger-icon\"]:has(+ [data-slot=\"accordion-trigger-icon\"]) {\n    rotate: 180deg;\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"accordion-trigger\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *)\n    > [data-slot=\"accordion-trigger-icon\"] + [data-slot=\"accordion-trigger-icon\"] {\n    display: none;\n  }\n\n  /* A combobox's chevron turns while its list is open. */\n  :where(html[data-kf]:not([data-kf-responses=\"none\"])) [data-slot=\"combobox-trigger\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *) > svg {\n    transition: rotate calc(200ms * var(--kf-pace)) var(--kf-ease);\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"combobox-trigger\"]:is([data-popup-open], [aria-expanded=\"true\"]):not([data-cut=\"none\"], [data-cut=\"none\"] *)\n    > svg {\n    rotate: 180deg;\n  }\n\n  /* A close ✕ turns a quarter while pressed; arrows nudge toward where they go. */\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    :is(\n      [data-slot=\"dialog-close\"], [data-slot=\"sheet-close\"], [data-slot=\"toast-close\"], [data-slot=\"combobox-chip-remove\"],\n      [data-slot=\"combobox-clear\"], [data-slot=\"carousel-previous\"], [data-slot=\"carousel-next\"], [data-slot=\"pagination-link\"],\n      [data-slot=\"message-scroller-button\"], [data-slot=\"calendar\"] :is(.rdp-button_previous, .rdp-button_next)\n    ):not([data-cut=\"none\"], [data-cut=\"none\"] *)\n    > svg {\n    transition:\n      rotate calc(150ms * var(--kf-pace)) var(--kf-ease),\n      translate calc(150ms * var(--kf-pace)) var(--kf-ease);\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    :is([data-slot=\"dialog-close\"], [data-slot=\"sheet-close\"], [data-slot=\"toast-close\"], [data-slot=\"combobox-chip-remove\"], [data-slot=\"combobox-clear\"]):not([data-cut=\"none\"], [data-cut=\"none\"] *):active\n    > svg {\n    rotate: 90deg;\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    :is([data-slot=\"carousel-previous\"], [data-slot=\"pagination-link\"][aria-label*=\"previous\" i], [data-slot=\"calendar\"] .rdp-button_previous):not([data-cut=\"none\"], [data-cut=\"none\"] *):active\n    > svg {\n    translate: -2px 0;\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    :is([data-slot=\"carousel-next\"], [data-slot=\"pagination-link\"][aria-label*=\"next\" i], [data-slot=\"calendar\"] .rdp-button_next):not([data-cut=\"none\"], [data-cut=\"none\"] *):active\n    > svg {\n    translate: 2px 0;\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"message-scroller-button\"]:not([data-direction=\"start\"], [data-cut=\"none\"], [data-cut=\"none\"] *):active\n    > svg {\n    translate: 0 2px;\n  }\n\n  /* Switch: the thumb travels on the theme's curve and stretches toward where it's going while held. */\n  :where(html[data-kf]:not([data-kf-responses=\"none\"])) [data-slot=\"switch-thumb\"]:not([data-cut=\"none\"] *) {\n    transition-duration: calc(150ms * var(--kf-pace));\n    transition-timing-function: var(--kf-ease);\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"switch\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *):active\n    > [data-slot=\"switch-thumb\"] {\n    scale: 1.18 1;\n    transform-origin: left center;\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"switch\"]:is([data-checked]:not([data-checked=\"false\"]), [data-state=\"checked\"]):not([data-cut=\"none\"], [data-cut=\"none\"] *):active\n    > [data-slot=\"switch-thumb\"] {\n    transform-origin: right center;\n  }\n\n  /* Drag: the slider thumb grows while it's held and settles on release. */\n  :where(html[data-kf]:not([data-kf-responses=\"none\"])) [data-slot=\"slider-thumb\"]:not([data-cut=\"none\"] *) {\n    transition:\n      color var(--default-transition-duration, 150ms) var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1)),\n      box-shadow var(--default-transition-duration, 150ms) var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1)),\n      scale calc(150ms * var(--kf-pace)) var(--kf-ease);\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"slider\"]:is([data-dragging], :active):not([data-cut=\"none\"], [data-cut=\"none\"] *)\n    [data-slot=\"slider-thumb\"] {\n    scale: 1.25;\n    transition:\n      color var(--default-transition-duration, 150ms) var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1)),\n      box-shadow var(--default-transition-duration, 150ms) var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1)),\n      scale calc(110ms * var(--kf-pace)) var(--kf-ease);\n  }\n\n  /* Drag: a resize handle thickens and takes the focus colour while it's being dragged. A box-shadow, not\n     a scale, so the grip inside it keeps its shape. */\n  :where(html[data-kf]:not([data-kf-responses=\"none\"])) [data-slot=\"resizable-handle\"]:not([data-cut=\"none\"] *) {\n    transition:\n      background-color calc(150ms * var(--kf-pace)) var(--kf-ease),\n      box-shadow calc(150ms * var(--kf-pace)) var(--kf-ease);\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"resizable-handle\"][data-separator=\"active\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *) {\n    background-color: var(--ring);\n    box-shadow: 0 0 0 1px var(--ring);\n  }\n\n  /* Sidebar: the width follows the theme's curve, and the labels leave before it closes and arrive after\n     it opens, the way a J-cut lets the next scene's sound lead. Labels are matched by structure (a span in a\n     menu item's button or link) because a collapsible item's trigger takes over the button's data-slot. */\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"sidebar\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *)\n    :is([data-slot=\"sidebar-container\"], [data-slot=\"sidebar-gap\"]) {\n    transition-duration: calc(240ms * var(--kf-pace));\n    transition-timing-function: var(--kf-ease);\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"sidebar\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *)\n    :is([data-slot=\"sidebar-menu-item\"], [data-slot=\"sidebar-menu-sub-item\"]) > :is(button, a) > span {\n    transition: opacity calc(150ms * var(--kf-pace)) var(--kf-ease) calc(120ms * var(--kf-pace));\n  }\n\n  :where(html[data-kf]:not([data-kf-responses=\"none\"]))\n    [data-slot=\"sidebar\"][data-collapsible=\"icon\"]:not([data-cut=\"none\"], [data-cut=\"none\"] *)\n    :is([data-slot=\"sidebar-menu-item\"], [data-slot=\"sidebar-menu-sub-item\"]) > :is(button, a) > span {\n    opacity: 0;\n    transition: opacity calc(80ms * var(--kf-pace)) var(--kf-ease-exit);\n  }\n\n  /* Fill: a progress bar eases to its new value on the theme's curve. */\n  :where(html[data-kf]:not([data-kf-responses=\"none\"])) [data-slot=\"progress-indicator\"]:not([data-cut=\"none\"] *) {\n    transition-duration: calc(240ms * var(--kf-pace));\n    transition-timing-function: var(--kf-ease);\n  }\n}\n\n/* ---------- Reduced motion: same selectors; the cuts become short fades with no movement ---------- */\n\n@media (prefers-reduced-motion: reduce) {\n  :where(:root) {\n    --kf-cap: 120ms;\n  }\n\n  @keyframes kf-rack-in {\n    from { opacity: 0; }\n    to { opacity: 1; }\n  }\n  @keyframes kf-rack-out {\n    from { opacity: 1; }\n    to { opacity: 0; }\n  }\n  @keyframes kf-punch-in {\n    from { opacity: 0; }\n    to { opacity: 1; }\n  }\n  @keyframes kf-punch-out {\n    from { opacity: 1; }\n    to { opacity: 0; }\n  }\n  @keyframes kf-panel-in {\n    from { opacity: 0; transform: none; translate: none; }\n    to { opacity: 1; transform: none; translate: none; }\n  }\n  @keyframes kf-panel-out {\n    from { opacity: 1; transform: none; translate: none; }\n    to { opacity: 0; transform: none; translate: none; }\n  }\n  @keyframes kf-grow {\n    from { opacity: 0; }\n    to { opacity: 1; }\n  }\n  @keyframes kf-shrink {\n    from { opacity: 1; }\n    to { opacity: 0; }\n  }\n\n  /* The tuned components' own keyframes move (zoom, slide); under reduced motion they fade instead. */\n  :where(html[data-kf])\n    :is(\n      [data-slot=\"popover-content\"], [data-slot=\"dropdown-menu-content\"], [data-slot=\"dropdown-menu-sub-content\"],\n      [data-slot=\"context-menu-content\"], [data-slot=\"context-menu-sub-content\"], [data-slot=\"menubar-content\"],\n      [data-slot=\"menubar-sub-content\"], [data-slot=\"select-content\"], [data-slot=\"combobox-content\"],\n      [data-slot=\"hover-card-content\"], [data-slot=\"tooltip-content\"], [data-slot=\"navigation-menu-content\"],\n      [data-slot=\"navigation-menu-viewport\"], [data-slot=\"accordion-content\"]\n    ):not([data-cut=\"none\"], [data-cut=\"none\"] *):is([data-state=\"open\"], [data-state=\"delayed-open\"], [data-state=\"instant-open\"], [data-open]:not([data-open=\"false\"])) {\n    animation-name: kf-fade-in;\n    transition-property: opacity;\n  }\n\n  :where(html[data-kf])\n    :is(\n      [data-slot=\"popover-content\"], [data-slot=\"dropdown-menu-content\"], [data-slot=\"dropdown-menu-sub-content\"],\n      [data-slot=\"context-menu-content\"], [data-slot=\"context-menu-sub-content\"], [data-slot=\"menubar-content\"],\n      [data-slot=\"menubar-sub-content\"], [data-slot=\"select-content\"], [data-slot=\"combobox-content\"],\n      [data-slot=\"hover-card-content\"], [data-slot=\"tooltip-content\"], [data-slot=\"navigation-menu-content\"],\n      [data-slot=\"navigation-menu-viewport\"], [data-slot=\"accordion-content\"]\n    ):not([data-cut=\"none\"], [data-cut=\"none\"] *):is([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])) {\n    animation-name: kf-fade-out;\n    transition-property: opacity;\n  }\n\n  :where(html[data-kf]) [data-kf-sink=\"on\"] {\n    scale: 1;\n    filter: none;\n    border-radius: 0;\n  }\n  /* Responses: the tick and the radio dot fade in; nothing draws or grows. */\n  @keyframes kf-draw {\n    from { opacity: 0; }\n    to { opacity: 1; }\n  }\n  @keyframes kf-dot-in {\n    from { opacity: 0; }\n  }\n}\n",
      "type": "registry:file",
      "target": "components/keyframery/keyframery.css"
    },
    {
      "path": "lib/keyframery/engine.ts",
      "content": "/** Starts and stops the layer. Reference-counted: several <Cuts /> (or React StrictMode) are safe. */\n\nimport { appearAdapter } from \"./adapters/appear\"\nimport { arrivalAdapter } from \"./adapters/arrival\"\nimport { calendarAdapter } from \"./adapters/calendar\"\nimport { errorAdapter } from \"./adapters/error\"\nimport { highlightAdapter } from \"./adapters/highlight\"\nimport { overlayAdapter } from \"./adapters/overlay\"\nimport { panelAdapter } from \"./adapters/panel\"\nimport { stepsAdapter } from \"./adapters/steps\"\nimport { tabsAdapter } from \"./adapters/tabs\"\nimport { toastAdapter } from \"./adapters/toast\"\nimport { toggleAdapter } from \"./adapters/toggle\"\nimport { tunedAdapter } from \"./adapters/tuned\"\nimport { parsePace } from \"./motion\"\nimport { observe, type Adapter } from \"./observe\"\nimport { createPressTracker, type Press, type PressTracker } from \"./press\"\n\nexport type DialogCut = \"rack-focus\" | \"punch-in\" | \"fade\" | \"none\"\nexport type SheetCut = \"slide-sink\" | \"slide\" | \"fade\" | \"none\"\nexport type DrawerCut = \"slide-sink\" | \"none\"\nexport type TabsCut = \"j-cut\" | \"whip\" | \"fade\" | \"none\"\nexport type ToastCut = \"cut-on-action\" | \"none\"\nexport type Menus = { dialog?: DialogCut; sheet?: SheetCut; drawer?: DrawerCut; tabs?: TabsCut; toast?: ToastCut }\n\nexport const DEFAULT_MENUS: Required<Menus> = {\n  dialog: \"rack-focus\",\n  sheet: \"slide-sink\",\n  drawer: \"slide-sink\",\n  tabs: \"j-cut\",\n  toast: \"cut-on-action\",\n}\n\n/** Every adapter the engine runs. Each cut group adds its factory here. */\nexport const ADAPTER_FACTORIES: Array<(press: PressTracker) => Adapter> = [overlayAdapter, panelAdapter, tabsAdapter, toastAdapter, tunedAdapter, toggleAdapter, highlightAdapter, errorAdapter, arrivalAdapter, appearAdapter, calendarAdapter, stepsAdapter]\n\nlet refs = 0\nlet activePress: PressTracker | null = null\nlet stopEngine: (() => void) | null = null\n\nfunction applyPace(el: HTMLElement) {\n  el.style.setProperty(\"--kf-pace\", String(parsePace(el.getAttribute(\"data-cut-pace\"))))\n}\n\nconst paceAdapter: Adapter = {\n  match: \"[data-cut-pace]\",\n  init: () => document.querySelectorAll<HTMLElement>(\"[data-cut-pace]\").forEach(applyPace),\n  added: applyPace,\n  changed: (el, attribute) => {\n    if (attribute === \"data-cut-pace\") applyPace(el)\n  },\n}\n\nfunction install(): () => void {\n  const press = createPressTracker(document)\n  activePress = press\n  const adapters = [paceAdapter, ...ADAPTER_FACTORIES.map((make) => make(press))]\n  const disconnect = observe(document.body, adapters)\n  adapters.forEach((a) => a.init?.())\n  return () => {\n    activePress = null\n    disconnect()\n    adapters.forEach((a) => a.dispose?.())\n    press.dispose()\n    const html = document.documentElement\n    for (const name of [...html.getAttributeNames()]) if (name === \"data-kf\" || name.startsWith(\"data-kf-\")) html.removeAttribute(name)\n    html.style.removeProperty(\"--kf-pace\")\n  }\n}\n\nexport function start(): () => void {\n  refs += 1\n  if (refs === 1) stopEngine = install()\n  let released = false\n  return () => {\n    if (released) return\n    released = true\n    refs -= 1\n    if (refs === 0 && stopEngine) {\n      stopEngine()\n      stopEngine = null\n    }\n  }\n}\n\nexport function applyMenus(menus: Menus): void {\n  const html = document.documentElement\n  for (const [group, value] of Object.entries({ ...DEFAULT_MENUS, ...stripUndefined(menus) })) html.setAttribute(`data-kf-${group}`, value)\n}\n\nfunction stripUndefined(menus: Menus): Menus {\n  return Object.fromEntries(Object.entries(menus).filter(([, v]) => v !== undefined)) as Menus\n}\n\nexport function setEnabled(on: boolean): void {\n  if (on) document.documentElement.setAttribute(\"data-kf\", \"\")\n  else document.documentElement.removeAttribute(\"data-kf\")\n}\n\n/** \"none\" turns off every response (press, toggle, drag, fill); cuts keep playing. */\nexport type Responses = \"on\" | \"none\"\n\nexport function setResponses(responses: Responses | undefined): void {\n  if (responses === \"none\") document.documentElement.setAttribute(\"data-kf-responses\", \"none\")\n  else document.documentElement.removeAttribute(\"data-kf-responses\")\n}\n\nexport function setPace(pace: number | undefined): void {\n  if (pace === undefined) document.documentElement.style.removeProperty(\"--kf-pace\")\n  else document.documentElement.style.setProperty(\"--kf-pace\", String(parsePace(String(pace))))\n}\n\n/** The last press, for helpers that cut from what the user pressed. Null when no <Cuts /> is running. */\nexport function lastPress(maxAgeMs?: number): Press | null {\n  return activePress?.last(maxAgeMs) ?? null\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/engine.ts"
    },
    {
      "path": "lib/keyframery/state.ts",
      "content": "/** Reads open / closed / active the same way across Base UI and Radix. */\n\nexport const OPEN =\n  '[data-state=\"open\"],[data-state=\"delayed-open\"],[data-state=\"instant-open\"],[data-open]:not([data-open=\"false\"])'\nexport const CLOSED = '[data-state=\"closed\"],[data-closed]:not([data-closed=\"false\"])'\nexport const ACTIVE = '[data-state=\"active\"],[data-active]:not([data-active=\"false\"]),[aria-selected=\"true\"]'\n\nexport type Phase = \"open\" | \"closed\"\n\nexport function phaseOf(el: Element): Phase | null {\n  if (el.matches(OPEN)) return \"open\"\n  if (el.matches(CLOSED)) return \"closed\"\n  return null\n}\n\nexport function isActive(el: Element): boolean {\n  return el.matches(ACTIVE)\n}\n\n/** True while <Cuts /> is disabled, or when this element or an ancestor says data-cut=\"none\". */\nexport function optedOut(el: Element): boolean {\n  return !document.documentElement.hasAttribute(\"data-kf\") || el.closest('[data-cut=\"none\"]') !== null\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/state.ts"
    },
    {
      "path": "lib/keyframery/motion.ts",
      "content": "/** Timing, easing, pace, reduced motion, and the keyframery:cut event. Keep in step with keyframery.css. */\n\nexport const DURATIONS = {\n  \"rack-focus\": { enter: 340, exit: 220 },\n  \"punch-in\": { enter: 260, exit: 160 },\n  fade: { enter: 200, exit: 150 },\n  \"slide-sink\": { enter: 460, exit: 260 },\n  slide: { enter: 460, exit: 260 },\n  tuned: { enter: 180, exit: 180 },\n  \"cut-on-action\": { enter: 480, exit: 0 },\n} as const\n\nexport const REDUCED_MS = 120\n\nexport type CutPhase = \"enter\" | \"exit\"\nexport type CutEventDetail = { cut: string; component: string; phase: CutPhase; ms: number }\n\nexport function parsePace(raw: string | null | undefined): number {\n  const n = Number.parseFloat(raw ?? \"\")\n  return Number.isFinite(n) && n > 0 ? n : 1\n}\n\nexport function paceOf(el: Element): number {\n  return parsePace(getComputedStyle(el).getPropertyValue(\"--kf-pace\"))\n}\n\nconst EASING = { enter: \"cubic-bezier(0.22, 1, 0.36, 1)\", exit: \"cubic-bezier(0.5, 0, 0.75, 0)\" }\n\nfunction validEasing(value: string): boolean {\n  // CSS accepts global keywords and comma-separated animation lists; WAAPI needs one easing.\n  if (!value || /^(inherit|initial|unset|revert|revert-layer)$/i.test(value) || /\\b(var|env)\\(/i.test(value)) return false\n  let depth = 0\n  for (const char of value) {\n    if (char === \"(\") depth++\n    else if (char === \")\") depth--\n    else if (char === \",\" && depth === 0) return false\n  }\n  return typeof CSS !== \"undefined\" && CSS.supports(\"animation-timing-function\", value)\n}\n\n/** Resolve a scoped theme curve, without letting invalid custom properties throw in animate(). */\nexport function easingOf(el: Element, phase: CutPhase = \"enter\", fallback = EASING[phase]): string {\n  const value = getComputedStyle(el).getPropertyValue(phase === \"exit\" ? \"--kf-ease-exit\" : \"--kf-ease\").trim()\n  return validEasing(value) ? value : validEasing(fallback) ? fallback : EASING[phase]\n}\n\nexport function reducedMotion(): boolean {\n  return typeof matchMedia === \"function\" && matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n}\n\n/** A base duration for this element: × its pace, or capped to a short fade under reduced motion. */\nexport function scaled(baseMs: number, el: Element): number {\n  return reducedMotion() ? Math.min(baseMs, REDUCED_MS) : baseMs * paceOf(el)\n}\n\nexport function emitCut(detail: CutEventDetail): void {\n  document.dispatchEvent(new CustomEvent<CutEventDetail>(\"keyframery:cut\", { detail }))\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/motion.ts"
    },
    {
      "path": "lib/keyframery/press.ts",
      "content": "/** Remembers what the user last pressed: the element that started a cut. */\n\n/** `kind` says whether a pointer or a key started it; `key` names the key. */\nexport type Press = { el: Element; rect: DOMRect; t: number; kind: \"pointer\" | \"key\"; key?: string }\n\nexport type PressTracker = {\n  /** The last press, if it happened within `maxAgeMs`. */\n  last(maxAgeMs?: number): Press | null\n  /** Called on every press, before the library handles it (capture phase). */\n  onPress(fn: (press: Press, target: Element) => void): () => void\n  dispose(): void\n}\n\nconst PRESSABLE = 'button,a,input,textarea,[role=\"button\"],[role=\"menuitem\"],[role=\"tab\"],[role=\"option\"],[data-slot$=\"-trigger\"]'\nconst KEYS = new Set([\"Enter\", \" \", \"ArrowLeft\", \"ArrowRight\", \"ArrowUp\", \"ArrowDown\", \"Home\", \"End\"])\n\nexport function createPressTracker(doc: Document = document): PressTracker {\n  let last: Press | null = null\n  const subs = new Set<(press: Press, target: Element) => void>()\n\n  const handle = (e: Event) => {\n    const target = e.target instanceof Element ? e.target : null\n    if (!target) return\n    if (e instanceof KeyboardEvent && !KEYS.has(e.key)) return\n    const el = target.closest(PRESSABLE) ?? target\n    const kind = e instanceof KeyboardEvent ? \"key\" : \"pointer\"\n    const press: Press = { el, rect: el.getBoundingClientRect(), t: performance.now(), kind, key: e instanceof KeyboardEvent ? e.key : undefined }\n    last = press\n    subs.forEach((fn) => fn(press, target))\n  }\n\n  doc.addEventListener(\"pointerdown\", handle, true)\n  doc.addEventListener(\"keydown\", handle, true)\n\n  return {\n    last(maxAgeMs = 1500) {\n      return last && performance.now() - last.t < maxAgeMs ? last : null\n    },\n    onPress(fn) {\n      subs.add(fn)\n      return () => {\n        subs.delete(fn)\n      }\n    },\n    dispose() {\n      doc.removeEventListener(\"pointerdown\", handle, true)\n      doc.removeEventListener(\"keydown\", handle, true)\n      subs.clear()\n    },\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/press.ts"
    },
    {
      "path": "lib/keyframery/aim.ts",
      "content": "/** Points a cut at the element that started it, and carries that element's settings onto portaled content. */\n\nimport type { Press } from \"./press\"\n\nexport type RectLike = { left: number; top: number; width: number; height: number }\n\nexport const KF_VARS = [\"--kf-pace\", \"--kf-ease\", \"--kf-ease-exit\", \"--kf-travel\", \"--kf-blur\", \"--kf-depth\", \"--kf-hold\"] as const\n\nexport function centerOf(r: RectLike) {\n  return { x: r.left + r.width / 2, y: r.top + r.height / 2 }\n}\n\n/** Vector from the centre of `to` to the centre of `from`, in px. */\nexport function offsetBetween(from: RectLike, to: RectLike) {\n  const a = centerOf(from)\n  const b = centerOf(to)\n  const round = (n: number) => Math.round(n * 100) / 100\n  return { dx: round(a.x - b.x), dy: round(a.y - b.y) }\n}\n\n/**\n * The element's resting box: an enter animation's first frame is transformed, so measure with every\n * animation seeked to its end, then put each one back exactly. Nothing is cancelled or restarted, so the\n * libraries that watch these animations (Radix Presence, Base UI's finished promises) never notice.\n */\nexport function restingRect(el: HTMLElement): DOMRect {\n  const saved = el.getAnimations().map((a) => ({ a, t: a.currentTime, state: a.playState }))\n  for (const { a } of saved) {\n    a.pause()\n    a.currentTime = Number(a.effect?.getComputedTiming().endTime ?? 0)\n  }\n  const rect = el.getBoundingClientRect()\n  for (const { a, t, state } of saved) {\n    a.currentTime = t ?? 0\n    if (state === \"finished\") a.finish()\n    else if (state !== \"paused\") a.play()\n  }\n  return rect\n}\n\n/** Writes --kf-dx/--kf-dy so the CSS cut starts toward the opener. No opener (or data-cut-origin) = from the centre. */\nexport function aimAt(el: HTMLElement, opener: Press | null, atRest: boolean): void {\n  if (!opener || el.hasAttribute(\"data-cut-origin\")) {\n    el.style.setProperty(\"--kf-dx\", \"0px\")\n    el.style.setProperty(\"--kf-dy\", \"0px\")\n    return\n  }\n  const from = opener.el.isConnected ? opener.el.getBoundingClientRect() : opener.rect\n  const to = atRest ? restingRect(el) : el.getBoundingClientRect()\n  const { dx, dy } = offsetBetween(from, to)\n  el.style.setProperty(\"--kf-dx\", `${dx}px`)\n  el.style.setProperty(\"--kf-dy\", `${dy}px`)\n}\n\n/**\n * Portaled content renders at the end of <body>, outside the section it was opened from.\n * Copy the opener's opt-out and its --kf-* values onto it, only where they differ.\n * Named cuts are never carried: a tabs cut means nothing to a dialog.\n */\nexport function carrySettings(content: HTMLElement, opener: Element | null): void {\n  if (!opener || content.contains(opener)) return\n  if (!content.hasAttribute(\"data-cut\") && opener.closest('[data-cut=\"none\"]')) content.setAttribute(\"data-cut\", \"none\")\n  const from = getComputedStyle(opener)\n  const here = getComputedStyle(content)\n  for (const name of KF_VARS) {\n    const value = from.getPropertyValue(name).trim()\n    if (value && value !== here.getPropertyValue(name).trim()) content.style.setProperty(name, value)\n  }\n}\n\n/** A per-dialog choice also applies to its backdrop, which renders as an earlier sibling. */\nexport function mirrorCut(content: HTMLElement, overlaySelector: string): void {\n  const value = content.getAttribute(\"data-cut\")\n  if (!value) return\n  let node = content.previousElementSibling\n  while (node && !node.matches(overlaySelector)) node = node.previousElementSibling\n  const overlay = node ?? content.parentElement?.querySelector(overlaySelector) ?? null\n  if (overlay && !overlay.hasAttribute(\"data-cut\")) overlay.setAttribute(\"data-cut\", value)\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/aim.ts"
    },
    {
      "path": "lib/keyframery/ghost.ts",
      "content": "/** A stand-in copy of an element that is leaving, so no library has to keep the real one mounted. */\n\nexport type Snapshot = { clone: HTMLElement; rect: DOMRect }\n\nexport function snapshot(el: Element): Snapshot {\n  const clone = el.cloneNode(true) as HTMLElement\n  clone.removeAttribute(\"id\")\n  clone.querySelectorAll(\"[id]\").forEach((n) => n.removeAttribute(\"id\"))\n  const slot = clone.getAttribute(\"data-slot\")\n  if (slot) clone.setAttribute(\"data-slot\", `${slot}-ghost`)\n  clone.setAttribute(\"data-kf-ghost\", \"\")\n  return { clone, rect: el.getBoundingClientRect() }\n}\n\n/**\n * Places the ghost over the element's last box, plays `keyframes`, and always removes it afterwards.\n * With `place.host` (an ancestor of the leaving element) the ghost lives inside it, absolutely positioned,\n * so it stays in the same stacking context: tabs inside a dialog keep their ghost above the dialog.\n */\nexport function playGhost(snap: Snapshot, keyframes: Keyframe[], options: KeyframeAnimationOptions, place: { host?: HTMLElement; z?: number } = {}): Animation {\n  const host = place.host\n  const g = snap.clone\n  let left = snap.rect.left\n  let top = snap.rect.top\n  if (host) {\n    const box = host.getBoundingClientRect()\n    left = snap.rect.left - box.left - host.clientLeft + host.scrollLeft\n    top = snap.rect.top - box.top - host.clientTop + host.scrollTop\n    if (getComputedStyle(host).position === \"static\") {\n      host.style.position = \"relative\"\n      host.setAttribute(\"data-kf-relative\", \"\")\n    }\n  }\n  Object.assign(g.style, {\n    position: host ? \"absolute\" : \"fixed\",\n    left: `${left}px`,\n    top: `${top}px`,\n    width: `${snap.rect.width}px`,\n    height: `${snap.rect.height}px`,\n    margin: \"0\",\n    boxSizing: \"border-box\",\n    pointerEvents: \"none\",\n    zIndex: String(place.z ?? 1),\n  })\n  g.hidden = false\n  g.setAttribute(\"aria-hidden\", \"true\")\n  g.inert = true\n  ;(host ?? document.body).appendChild(g)\n  const anim = g.animate(keyframes, { fill: \"forwards\", ...options })\n  const done = () => {\n    g.remove()\n    if (host?.hasAttribute(\"data-kf-relative\") && !host.querySelector(\":scope > [data-kf-ghost]\")) {\n      host.style.removeProperty(\"position\")\n      host.removeAttribute(\"data-kf-relative\")\n    }\n  }\n  anim.finished.then(done, done)\n  return anim\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/ghost.ts"
    },
    {
      "path": "lib/keyframery/observe.ts",
      "content": "/** One MutationObserver for the whole page; hands each change to the adapter that owns it. */\n\nexport type Adapter = {\n  /** Selector for the elements this adapter owns. */\n  match: string\n  /** Runs once at start, for markup that was already in the page. */\n  init?(): void\n  /** `root` is the node that was inserted: the element itself, or an ancestor inserted with it. */\n  added?(el: HTMLElement, root: Element): void\n  changed?(el: HTMLElement, attribute: string): void\n  removed?(el: HTMLElement): void\n  /** Runs once after every mutation batch. */\n  flush?(): void\n  /** Runs when the engine stops. */\n  dispose?(): void\n}\n\nexport const WATCHED_ATTRIBUTES = [\"data-state\", \"data-open\", \"data-closed\", \"hidden\", \"data-active\", \"aria-selected\", \"data-cut\", \"data-cut-pace\", \"data-checked\", \"data-unchecked\", \"data-pressed\", \"aria-pressed\", \"aria-current\", \"data-selected-single\", \"aria-invalid\", \"data-invalid\", \"data-selected\"]\n\nconst inGhost = (n: Element) => n.closest(\"[data-kf-ghost]\") !== null\n\nfunction eachMatch(node: Element, selector: string, fn: (el: HTMLElement) => void) {\n  if (node.matches(selector)) fn(node as HTMLElement)\n  node.querySelectorAll<HTMLElement>(selector).forEach(fn)\n}\n\n/** An adapter bug must never break the page: skip the cut, keep the library's own motion. */\nfunction safely(fn: () => void) {\n  try {\n    fn()\n  } catch (err) {\n    console.warn(\"[keyframery] cut skipped:\", err)\n  }\n}\n\nexport function observe(root: HTMLElement, adapters: Adapter[]): () => void {\n  const perf = (globalThis as { __kfPerf?: number[] }).__kfPerf\n  const mo = new MutationObserver((records) => {\n    const t0 = performance.now()\n    for (const r of records) {\n      if (r.type === \"childList\") {\n        r.addedNodes.forEach((n) => {\n          if (!(n instanceof Element) || inGhost(n)) return\n          for (const a of adapters) if (a.added) eachMatch(n, a.match, (el) => safely(() => a.added!(el, n)))\n        })\n        r.removedNodes.forEach((n) => {\n          if (!(n instanceof Element) || n.hasAttribute(\"data-kf-ghost\")) return\n          for (const a of adapters) if (a.removed) eachMatch(n, a.match, (el) => safely(() => a.removed!(el)))\n        })\n      } else if (r.target instanceof HTMLElement && !inGhost(r.target)) {\n        const el = r.target\n        for (const a of adapters) if (a.changed && el.matches(a.match)) safely(() => a.changed!(el, r.attributeName ?? \"\"))\n      }\n    }\n    for (const a of adapters) if (a.flush) safely(() => a.flush!())\n    perf?.push(performance.now() - t0)\n  })\n  mo.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: WATCHED_ATTRIBUTES })\n  return () => mo.disconnect()\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/observe.ts"
    },
    {
      "path": "lib/keyframery/adapters/overlay.ts",
      "content": "/** Dialog, alert dialog and command: aim the cut at the opener, carry section settings, report the cut. */\n\nimport { aimAt, carrySettings, mirrorCut } from \"../aim\"\nimport { DURATIONS, emitCut, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport type { Press, PressTracker } from \"../press\"\nimport { optedOut, phaseOf, type Phase } from \"../state\"\n\nconst CONTENT = '[data-slot=\"dialog-content\"],[data-slot=\"alert-dialog-content\"]'\nconst OVERLAY = '[data-slot=\"dialog-overlay\"],[data-slot=\"alert-dialog-overlay\"]'\nconst CUTS = [\"rack-focus\", \"punch-in\", \"fade\"] as const\ntype DialogCssCut = (typeof CUTS)[number]\n\nexport function dialogCutOf(el: Element): string {\n  return el.getAttribute(\"data-cut\") ?? document.documentElement.getAttribute(\"data-kf-dialog\") ?? \"rack-focus\"\n}\n\nfunction componentOf(el: Element): string {\n  if (el.querySelector('[data-slot=\"command\"]')) return \"command\"\n  return el.getAttribute(\"data-slot\") === \"alert-dialog-content\" ? \"alert-dialog\" : \"dialog\"\n}\n\nexport function overlayAdapter(press: PressTracker): Adapter {\n  const seen = new WeakMap<Element, Phase>()\n  const openers = new WeakMap<Element, Press | null>()\n\n  const handle = (el: HTMLElement, mounted: boolean) => {\n    const phase = phaseOf(el)\n    if (!phase || seen.get(el) === phase) return\n    seen.set(el, phase)\n    if (phase === \"open\") {\n      const p = press.last()\n      const opener = p && !el.contains(p.el) ? p : null\n      openers.set(el, opener)\n      carrySettings(el, opener?.el ?? null)\n      mirrorCut(el, OVERLAY)\n      if (optedOut(el)) return\n      aimAt(el, opener, true)\n    } else {\n      if (mounted || optedOut(el)) return\n      aimAt(el, openers.get(el) ?? null, false)\n    }\n    const cut = dialogCutOf(el)\n    if (!(CUTS as readonly string[]).includes(cut)) return\n    const d = DURATIONS[cut as DialogCssCut]\n    emitCut({ cut, component: componentOf(el), phase: phase === \"open\" ? \"enter\" : \"exit\", ms: Math.round(scaled(phase === \"open\" ? d.enter : d.exit, el)) })\n  }\n\n  return {\n    match: CONTENT,\n    added: (el) => handle(el, true),\n    changed: (el, attribute) => {\n      if (attribute === \"data-state\" || attribute === \"data-open\" || attribute === \"data-closed\") handle(el, false)\n    },\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/overlay.ts"
    },
    {
      "path": "lib/keyframery/adapters/panel.ts",
      "content": "/** Sheet and drawer: the page behind steps back while the panel is up. */\n\nimport { carrySettings, mirrorCut } from \"../aim\"\nimport { DURATIONS, emitCut, reducedMotion, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport type { PressTracker } from \"../press\"\nimport { optedOut, phaseOf, type Phase } from \"../state\"\n\nconst PANELS = '[data-slot=\"sheet-content\"],[data-slot=\"drawer-popup\"],[data-slot=\"drawer-content\"][data-vaul-drawer]'\nconst SHEET_OVERLAY = '[data-slot=\"sheet-overlay\"]'\n/** Top-level children of <body> that are portals or plumbing, never the app. */\nconst NOT_APP = [\n  \"script\", \"style\", \"template\", \"link\", \"next-route-announcer\", \"[hidden]\",\n  \"[data-base-ui-portal]\", \"[data-radix-popper-content-wrapper]\", \"[data-radix-focus-guard]\",\n  '[data-slot$=\"-content\"]', '[data-slot$=\"-overlay\"]', '[data-slot$=\"-popup\"]', '[data-slot$=\"-portal\"]', '[data-slot$=\"-viewport\"]',\n  \"[data-kf-ghost]\", \"[data-sonner-toaster]\", 'section[aria-label^=\"Notifications\"]',\n].join(\",\")\nconst SHEET_CUTS = [\"slide-sink\", \"slide\", \"fade\"] as const\ntype SheetCssCut = (typeof SHEET_CUTS)[number]\n\nconst groupOf = (el: Element): \"sheet\" | \"drawer\" => (el.getAttribute(\"data-slot\") === \"sheet-content\" ? \"sheet\" : \"drawer\")\n\nexport function panelCutOf(el: Element): string {\n  if (optedOut(el)) return \"none\"\n  return el.getAttribute(\"data-cut\") ?? document.documentElement.getAttribute(`data-kf-${groupOf(el)}`) ?? \"slide-sink\"\n}\n\n/** The top-level child of <body> that holds the opener. If the opener lives in a portal (a nested panel), nothing sinks again. */\nexport function appRootFor(opener: Element | null): HTMLElement | null {\n  let top: Element | null = opener\n  while (top && top.parentElement && top.parentElement !== document.body) top = top.parentElement\n  if (top && top.parentElement === document.body) return top.matches(NOT_APP) ? null : (top as HTMLElement)\n  return (Array.from(document.body.children).find((c) => !c.matches(NOT_APP)) as HTMLElement | undefined) ?? null\n}\n\nexport function panelAdapter(press: PressTracker): Adapter {\n  const seen = new WeakMap<Element, Phase>()\n  const sunk = new Map<Element, HTMLElement>()\n\n  const sinkOn = (panel: HTMLElement, opener: Element | null) => {\n    if (sunk.has(panel) || reducedMotion() || panelCutOf(panel) !== \"slide-sink\") return\n    const root = appRootFor(opener)\n    if (!root || root.contains(panel)) return\n    root.setAttribute(\"data-kf-sink\", \"on\")\n    document.documentElement.setAttribute(\"data-kf-sinking\", \"\")\n    sunk.set(panel, root)\n  }\n\n  const sinkOff = (panel: Element) => {\n    const root = sunk.get(panel)\n    if (!root) return\n    sunk.delete(panel)\n    if (![...sunk.values()].includes(root)) root.setAttribute(\"data-kf-sink\", \"off\")\n    if (sunk.size === 0) document.documentElement.removeAttribute(\"data-kf-sinking\")\n  }\n\n  const handle = (el: HTMLElement, mounted: boolean) => {\n    const phase = phaseOf(el)\n    if (!phase || seen.get(el) === phase) return\n    seen.set(el, phase)\n    if (phase === \"closed\") {\n      sinkOff(el)\n      if (mounted) return\n    } else {\n      const p = press.last()\n      const opener = p && !el.contains(p.el) ? p.el : null\n      carrySettings(el, opener)\n      mirrorCut(el, SHEET_OVERLAY)\n      sinkOn(el, opener)\n    }\n    const group = groupOf(el)\n    const cut = panelCutOf(el)\n    if (group === \"sheet\" ? !(SHEET_CUTS as readonly string[]).includes(cut) : cut !== \"slide-sink\") return\n    const d = DURATIONS[cut as SheetCssCut]\n    emitCut({ cut, component: group, phase: phase === \"open\" ? \"enter\" : \"exit\", ms: Math.round(scaled(phase === \"open\" ? d.enter : d.exit, el)) })\n  }\n\n  return {\n    match: PANELS,\n    added: (el) => handle(el, true),\n    changed: (el, attribute) => {\n      if (attribute === \"data-state\" || attribute === \"data-open\" || attribute === \"data-closed\") handle(el, false)\n    },\n    removed: sinkOff,\n    dispose: () => [...sunk.keys()].forEach(sinkOff),\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/panel.ts"
    },
    {
      "path": "lib/keyframery/adapters/tabs.ts",
      "content": "/** Tabs: the old panel leaves on a ghost, the tab pill whips across, the new panel follows, the frame resizes smoothly. */\n\nimport { playGhost, snapshot, type Snapshot } from \"../ghost\"\nimport { easingOf, emitCut, reducedMotion, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport type { PressTracker } from \"../press\"\nimport { isActive, optedOut } from \"../state\"\n\nconst ROOT = '[data-slot=\"tabs\"]'\nconst PANEL = '[data-slot=\"tabs-content\"]'\nconst TAB = '[data-slot=\"tabs-trigger\"]'\nconst TABS_CUTS = [\"j-cut\", \"whip\", \"fade\"] as const\ntype TabsCut = (typeof TABS_CUTS)[number]\n\ntype Snap = { panel: Element | null; ghost: Snapshot | null; height: number; fromIndex: number; fromTab: Element | null; fromRect: DOMRect | null; t: number }\n\nconst own = (root: Element, selector: string) => Array.from(root.querySelectorAll(selector)).filter((n) => n.closest(ROOT) === root)\nconst visible = (el: Element) => !(el as HTMLElement).hidden && el.getClientRects().length > 0\n\nexport function tabsCutOf(root: Element): TabsCut | \"none\" {\n  if (optedOut(root)) return \"none\"\n  const value = root.getAttribute(\"data-cut\") ?? document.documentElement.getAttribute(\"data-kf-tabs\") ?? \"j-cut\"\n  return (TABS_CUTS as readonly string[]).includes(value) ? (value as TabsCut) : \"none\"\n}\n\n/** A pill travels from the old tab to the new one; the real tab hides its own pill until it lands. */\nfunction whip(to: Element, fromRect: DOMRect, vertical: boolean, ms: number) {\n  const list = to.parentElement\n  if (!list) return\n  const toRect = to.getBoundingClientRect()\n  const listRect = list.getBoundingClientRect()\n  const cs = getComputedStyle(to)\n  const pill = document.createElement(\"span\")\n  pill.setAttribute(\"aria-hidden\", \"true\")\n  pill.setAttribute(\"data-kf-pill\", \"\")\n  Object.assign(pill.style, {\n    position: \"absolute\",\n    left: `${toRect.left - listRect.left - list.clientLeft}px`,\n    top: `${toRect.top - listRect.top - list.clientTop}px`,\n    width: `${toRect.width}px`,\n    height: `${toRect.height}px`,\n    borderRadius: cs.borderRadius,\n    background: cs.backgroundColor,\n    boxShadow: cs.boxShadow,\n    pointerEvents: \"none\",\n  })\n  if (getComputedStyle(list).position === \"static\") {\n    list.style.position = \"relative\"\n    list.setAttribute(\"data-kf-relative\", \"\")\n  }\n  list.prepend(pill)\n  const dx = fromRect.left + fromRect.width / 2 - (toRect.left + toRect.width / 2)\n  const dy = fromRect.top + fromRect.height / 2 - (toRect.top + toRect.height / 2)\n  const sx = fromRect.width / toRect.width\n  const sy = fromRect.height / toRect.height\n  const at = (f: number) => (vertical ? `0 ${dy * f}px` : `${dx * f}px 0`)\n  const bare = { backgroundColor: \"transparent\", boxShadow: \"none\", borderColor: \"transparent\" }\n  ;(to as HTMLElement).animate([bare, bare], { duration: ms })\n  const done = () => {\n    pill.remove()\n    if (list.hasAttribute(\"data-kf-relative\") && !list.querySelector(\":scope > [data-kf-pill]\")) {\n      list.style.removeProperty(\"position\")\n      list.removeAttribute(\"data-kf-relative\")\n    }\n  }\n  pill\n    .animate(\n      [\n        { translate: at(1), scale: vertical ? `1 ${sy}` : `${sx} 1` },\n        { translate: at(0.12), scale: vertical ? \"1 1.08\" : \"1.08 1\", offset: 0.7 },\n        { translate: \"0 0\", scale: \"1 1\" },\n      ],\n      // The stretch keyframe at 0.7 is timed for this accelerate-then-settle curve; it is the whip, so the theme curve doesn't replace it.\n      { duration: ms, easing: \"cubic-bezier(0.65, 0, 0.35, 1)\" },\n    )\n    .finished.then(done, done)\n}\n\nexport function tabsAdapter(press: PressTracker): Adapter {\n  const snaps = new Map<Element, Snap>()\n  const dirty = new Set<Element>()\n  const morphs = new WeakMap<Element, Animation>()\n\n  const unsubscribe = press.onPress((_p, target) => {\n    const root = target.closest(TAB)?.closest(ROOT)\n    if (!root || tabsCutOf(root) === \"none\") return\n    const panel = own(root, PANEL).find(visible) ?? null\n    const tabs = own(root, TAB)\n    const from = tabs.find(isActive) ?? null\n    snaps.set(root, {\n      panel,\n      ghost: panel ? snapshot(panel) : null,\n      height: root.getBoundingClientRect().height,\n      fromIndex: from ? tabs.indexOf(from) : -1,\n      fromTab: from,\n      fromRect: from?.getBoundingClientRect() ?? null,\n      t: performance.now(),\n    })\n  })\n\n  function play(root: Element, snap: Snap, to: Element) {\n    const cut = tabsCutOf(root)\n    if (cut === \"none\") return\n    const vertical = root.getAttribute(\"data-orientation\") === \"vertical\"\n    const dir = own(root, TAB).indexOf(to) >= snap.fromIndex ? 1 : -1\n    const controlled = to.getAttribute(\"aria-controls\")\n    const panel =\n      (controlled ? document.getElementById(controlled) : null) ??\n      own(root, PANEL).find((p) => p !== snap.panel && visible(p) && !p.hasAttribute(\"data-ending-style\")) ??\n      null\n\n    // Base UI keeps the old panel mounted for a frame (longer if it animates). The ghost plays that exit,\n    // so take the real one out of layout now and hand it back once the library is done with it.\n    if (snap.panel && snap.panel !== panel && visible(snap.panel)) {\n      const old = snap.panel as HTMLElement\n      old.style.display = \"none\"\n      requestAnimationFrame(() => requestAnimationFrame(() => old.style.removeProperty(\"display\")))\n    }\n\n    if (reducedMotion()) {\n      const ms = scaled(120, root)\n      panel?.animate([{ opacity: 0 }, { opacity: 1 }], { duration: ms, easing: easingOf(panel) })\n      emitCut({ cut, component: \"tabs\", phase: \"enter\", ms: Math.round(ms) })\n      return\n    }\n\n    const travel = cut === \"fade\" ? 0 : 28\n    const shift = (n: number) => (vertical ? `0 ${n}px` : `${n}px 0`)\n    const blur = cut === \"fade\" ? \"blur(0)\" : \"blur(4px)\"\n    const ghostMs = scaled(cut === \"whip\" ? 160 : 180, root)\n    const enterMs = scaled(cut === \"j-cut\" ? 300 : cut === \"whip\" ? 160 : 180, root)\n    const lead = cut === \"j-cut\" ? scaled(70, root) : 0\n\n    if (snap.ghost) {\n      playGhost(\n        snap.ghost,\n        [\n          { opacity: 1, translate: \"0 0\", filter: \"blur(0)\" },\n          { opacity: 0, translate: shift(-dir * travel), filter: blur },\n        ],\n        { duration: ghostMs, easing: easingOf(root, \"exit\", \"cubic-bezier(0.4, 0, 1, 1)\") },\n        { host: root as HTMLElement },\n      )\n    }\n\n    if (cut !== \"fade\" && snap.fromRect) whip(to, snap.fromRect, vertical, scaled(cut === \"whip\" ? 160 : 200, root))\n\n    morphs.get(root)?.cancel()\n    const height = root.getBoundingClientRect().height\n    if (Math.abs(height - snap.height) > 1) {\n      morphs.set(\n        root,\n        (root as HTMLElement).animate(\n          [\n            { height: `${snap.height}px`, overflow: \"clip\" },\n            { height: `${height}px`, overflow: \"clip\" },\n          ],\n          { duration: scaled(320, root), easing: easingOf(root) },\n        ),\n      )\n    }\n\n    panel?.animate(\n      [\n        { opacity: 0, translate: shift(dir * travel), filter: cut === \"whip\" ? \"blur(4px)\" : \"blur(0)\" },\n        { opacity: 1, translate: \"0 0\", filter: \"blur(0)\" },\n      ],\n      { duration: enterMs, delay: lead, easing: easingOf(panel), fill: \"backwards\" },\n    )\n    emitCut({ cut, component: \"tabs\", phase: \"enter\", ms: Math.round(enterMs + lead) })\n  }\n\n  const mark = (el: Element) => {\n    const root = el.closest(ROOT)\n    if (root) dirty.add(root)\n  }\n\n  return {\n    match: `${ROOT},${PANEL},${TAB}`,\n    added: mark,\n    changed: mark,\n    flush() {\n      for (const root of dirty) {\n        const snap = snaps.get(root)\n        if (!snap || performance.now() - snap.t > 1500) continue\n        const to = own(root, TAB).find(isActive)\n        if (!to || to === snap.fromTab) continue\n        snaps.delete(root)\n        play(root, snap, to)\n      }\n      dirty.clear()\n    },\n    dispose: unsubscribe,\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/tabs.ts"
    },
    {
      "path": "lib/keyframery/adapters/toast.ts",
      "content": "/** Toasts (sonner and Base UI): cut on action, so the toast leaves the element you pressed. */\n\nimport { centerOf } from \"../aim\"\nimport { DURATIONS, easingOf, emitCut, reducedMotion, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport type { PressTracker } from \"../press\"\nimport { optedOut } from \"../state\"\n\nconst TOAST = '[data-sonner-toast],[data-slot=\"toast\"]'\n\nexport function toastAdapter(press: PressTracker): Adapter {\n  const flown = new WeakSet<Element>()\n  return {\n    match: TOAST,\n    added(el) {\n      if (flown.has(el)) return\n      flown.add(el)\n      if (document.documentElement.getAttribute(\"data-kf-toast\") === \"none\" || reducedMotion() || optedOut(el)) return\n      const p = press.last(1000)\n      if (!p || el.contains(p.el) || optedOut(p.el)) return\n      const from = centerOf(p.el.isConnected ? p.el.getBoundingClientRect() : p.rect)\n      const to = centerOf(el.getBoundingClientRect())\n      const ms = scaled(DURATIONS[\"cut-on-action\"].enter, el)\n      // The individual translate/scale properties compose with the library's own transform transition instead of fighting it.\n      el.animate(\n        [\n          { translate: `${from.x - to.x}px ${from.y - to.y}px`, scale: \"0.4\", filter: \"blur(6px)\" },\n          { translate: \"0 0\", scale: \"1\", filter: \"blur(0)\" },\n        ],\n        { duration: ms, easing: easingOf(el) },\n      )\n      emitCut({ cut: \"cut-on-action\", component: el.hasAttribute(\"data-sonner-toast\") ? \"sonner\" : \"toast\", phase: \"enter\", ms: Math.round(ms) })\n    },\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/toast.ts"
    },
    {
      "path": "lib/keyframery/adapters/tuned.ts",
      "content": "/** The 11 tuned components are pure CSS. This carries section settings onto the portaled ones, keeps\n *  load-time collapsibles still, and reports each cut. */\n\nimport { carrySettings } from \"../aim\"\nimport { DURATIONS, emitCut, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport type { PressTracker } from \"../press\"\nimport { optedOut, phaseOf, type Phase } from \"../state\"\n\nexport const TUNED_SLOTS = [\n  \"popover-content\",\n  \"dropdown-menu-content\",\n  \"dropdown-menu-sub-content\",\n  \"context-menu-content\",\n  \"context-menu-sub-content\",\n  \"menubar-content\",\n  \"menubar-sub-content\",\n  \"select-content\",\n  \"combobox-content\",\n  \"hover-card-content\",\n  \"tooltip-content\",\n  \"navigation-menu-content\",\n  \"navigation-menu-viewport\",\n  \"accordion-content\",\n  \"collapsible-content\",\n] as const\n\nconst TUNED = TUNED_SLOTS.map((s) => `[data-slot=\"${s}\"]`).join(\",\")\nconst COLLAPSIBLE = '[data-slot=\"collapsible-content\"]'\nconst componentOf = (el: Element) => (el.getAttribute(\"data-slot\") ?? \"\").replace(/-(sub-content|content|viewport)$/, \"\")\n\nexport function tunedAdapter(press: PressTracker): Adapter {\n  const seen = new WeakMap<Element, Phase>()\n\n  const handle = (el: HTMLElement, mounted: boolean, initial: boolean) => {\n    const phase = phaseOf(el)\n    if (!phase || seen.get(el) === phase) return\n    seen.set(el, phase)\n    if (el.matches(COLLAPSIBLE)) {\n      if (phase === \"open\" && initial) el.setAttribute(\"data-kf-static\", \"\") // open before anyone pressed anything: no grow\n      if (phase === \"closed\") el.removeAttribute(\"data-kf-static\")\n    }\n    if (initial) return\n    if (phase === \"open\") {\n      const p = press.last()\n      carrySettings(el, p && !el.contains(p.el) ? p.el : null)\n    } else if (mounted) return\n    if (optedOut(el)) return\n    emitCut({ cut: \"tuned\", component: componentOf(el), phase: phase === \"open\" ? \"enter\" : \"exit\", ms: Math.round(scaled(DURATIONS.tuned.enter, el)) })\n  }\n\n  return {\n    match: TUNED,\n    init: () => document.querySelectorAll<HTMLElement>(TUNED).forEach((el) => handle(el, true, true)),\n    added: (el) => handle(el, true, false),\n    changed: (el, attribute) => {\n      if (attribute === \"data-state\" || attribute === \"data-open\" || attribute === \"data-closed\") handle(el, false, false)\n    },\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/tuned.ts"
    },
    {
      "path": "lib/keyframery/adapters/toggle.ts",
      "content": "/** Toggles: marks a control for a moment when its own state flips, so the CSS can draw the tick or grow\n *  the dot. Controls that load or mount already checked are never marked, so nothing redraws on page load\n *  or on a route change. */\n\nimport { emitCut, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport { optedOut } from \"../state\"\n\nexport const TOGGLE_SLOTS = [\n  \"checkbox\",\n  \"radio-group-item\",\n  \"switch\",\n  \"questionnaire-choice\",\n  \"dropdown-menu-checkbox-item\",\n  \"dropdown-menu-radio-item\",\n  \"context-menu-checkbox-item\",\n  \"context-menu-radio-item\",\n  \"menubar-checkbox-item\",\n  \"menubar-radio-item\",\n  \"command-item\",\n  \"combobox-item\",\n] as const\n\nconst TOGGLES = TOGGLE_SLOTS.map((s) => `[data-slot=\"${s}\"]`).join(\",\")\n// Base UI sets data-checked / data-unchecked; Radix sets data-state. A combobox item is \"on\" when selected.\n// (The command palette uses data-selected for its keyboard highlight, so only data-checked counts there.)\nconst ON = '[data-checked]:not([data-checked=\"false\"]),[data-state=\"checked\"]'\nconst ON_SELECTED = '[data-slot=\"combobox-item\"]:is([data-selected]:not([data-selected=\"false\"]))'\nconst isOn = (el: Element) => el.matches(ON) || el.matches(ON_SELECTED)\nconst STATE_ATTRIBUTES = new Set([\"data-checked\", \"data-unchecked\", \"data-state\", \"data-selected\"])\n// How long the mark stays after the response: long enough for a slowed-down pace to finish.\nconst HOLD_MS = 250\n\n/** True while <Cuts responses=\"none\" /> is set. */\nexport const responsesOff = () => document.documentElement.getAttribute(\"data-kf-responses\") === \"none\"\n\nexport function toggleAdapter(): Adapter {\n  const seen = new WeakMap<Element, boolean>()\n  const timers = new WeakMap<Element, ReturnType<typeof setTimeout>>()\n  const remember = (el: HTMLElement) => seen.set(el, isOn(el))\n\n  return {\n    match: TOGGLES,\n    init: () => document.querySelectorAll<HTMLElement>(TOGGLES).forEach(remember),\n    added: remember,\n    changed: (el, attribute) => {\n      if (!STATE_ATTRIBUTES.has(attribute)) return\n      const on = isOn(el)\n      if (seen.get(el) === on) return\n      seen.set(el, on)\n      if (optedOut(el) || responsesOff()) return\n      clearTimeout(timers.get(el))\n      el.setAttribute(\"data-kf-toggled\", on ? \"on\" : \"off\")\n      const ms = scaled(150, el)\n      timers.set(\n        el,\n        setTimeout(() => el.removeAttribute(\"data-kf-toggled\"), ms + HOLD_MS),\n      )\n      emitCut({ cut: \"toggle\", component: el.getAttribute(\"data-slot\") ?? \"toggle\", phase: on ? \"enter\" : \"exit\", ms: Math.round(ms) })\n    },\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/toggle.ts"
    },
    {
      "path": "lib/keyframery/adapters/highlight.ts",
      "content": "/** Highlight: when the selection moves to a sibling, its background travels there instead of jumping.\n *  Toggle groups, pagination, the sidebar and the calendar. A change made from the keyboard stays instant:\n *  people press arrow keys many times in a row, and motion would only slow them down. */\n\nimport { easingOf, emitCut, reducedMotion, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport type { PressTracker } from \"../press\"\nimport { optedOut } from \"../state\"\nimport { responsesOff } from \"./toggle\"\n\ntype Row = {\n  /** Where the travelling pill is drawn: an ancestor holding every item, never a table part. */\n  host: string\n  item: string\n  active: string\n}\n\nexport const HIGHLIGHTS: Row[] = [\n  {\n    host: '[data-slot=\"toggle-group\"]',\n    item: '[data-slot=\"toggle-group-item\"]',\n    active: '[data-pressed]:not([data-pressed=\"false\"]),[data-state=\"on\"]',\n  },\n  { host: '[data-slot=\"pagination-content\"]', item: '[data-slot=\"pagination-link\"]', active: '[aria-current=\"page\"]' },\n  {\n    host: '[data-slot=\"sidebar-content\"]',\n    item: '[data-slot=\"sidebar-menu-button\"],[data-slot=\"sidebar-menu-sub-button\"]',\n    active: '[data-active]:not([data-active=\"false\"])',\n  },\n  { host: '[data-slot=\"calendar\"]', item: \"[data-day]\", active: '[data-selected-single=\"true\"]' },\n]\n\nconst ITEMS = HIGHLIGHTS.map((r) => r.item).join(\",\")\nconst ATTRIBUTES = new Set([\"data-pressed\", \"aria-pressed\", \"data-state\", \"aria-current\", \"data-active\", \"data-selected-single\"])\n\ntype Flight = { pill: HTMLElement; anims: Animation[]; restore: () => void }\n\nfunction rowOf(item: Element): Row | undefined {\n  return HIGHLIGHTS.find((r) => item.matches(r.item))\n}\n\nfunction activeIn(host: Element, row: Row): Element[] {\n  return [...host.querySelectorAll(row.item)].filter((el) => el.matches(row.active) && host.contains(el))\n}\n\n/** Position of `r` inside `host`'s padding box, following its scroll. */\nfunction inside(host: HTMLElement, r: DOMRect) {\n  const h = host.getBoundingClientRect()\n  return {\n    left: `${r.left - h.left - host.clientLeft + host.scrollLeft}px`,\n    top: `${r.top - h.top - host.clientTop + host.scrollTop}px`,\n    width: `${r.width}px`,\n    height: `${r.height}px`,\n  }\n}\n\nexport function highlightAdapter(press: PressTracker): Adapter {\n  const last = new WeakMap<Element, Element | null>()\n  const flights = new WeakMap<Element, Flight>()\n  const dirty = new Set<HTMLElement>()\n  // Where the selection was when the user pressed inside a host. Some components rebuild on select (the\n  // calendar re-renders its whole grid, root included), so by the time the change shows, the old item and\n  // even the old host are gone; this remembers the spot by row, not by element.\n  let pressed: { row: Row; rect: DOMRect; t: number } | null = null\n  const unsubscribe = press.onPress((p) => {\n    pressed = null\n    for (const row of HIGHLIGHTS) {\n      const host = p.el.closest(row.host)\n      const current = host && activeIn(host, row)\n      if (current && current.length === 1) pressed = { row, rect: current[0].getBoundingClientRect(), t: performance.now() }\n    }\n  })\n\n  const record = (host: Element, row: Row) => {\n    const now = activeIn(host, row)\n    last.set(host, now.length === 1 ? now[0] : null)\n  }\n  const recordAll = () => {\n    for (const row of HIGHLIGHTS) document.querySelectorAll(row.host).forEach((host) => record(host, row))\n  }\n\n  const land = (host: Element) => {\n    const f = flights.get(host)\n    if (!f) return\n    flights.delete(host)\n    f.anims.forEach((a) => a.cancel())\n    f.pill.remove()\n    f.restore()\n  }\n\n  const glide = (host: HTMLElement, from: DOMRect, to: HTMLElement, inactiveColor: string) => {\n    const cs = getComputedStyle(to)\n    const toRect = to.getBoundingClientRect()\n    const distance = Math.hypot(toRect.left - from.left, toRect.top - from.top)\n    const ms = scaled(Math.min(320, Math.max(200, 200 + distance * 0.25)), to)\n    const pill = document.createElement(\"span\")\n    pill.setAttribute(\"aria-hidden\", \"true\")\n    pill.setAttribute(\"data-kf-pill\", \"\")\n    Object.assign(pill.style, {\n      position: \"absolute\",\n      zIndex: \"-1\",\n      boxSizing: \"border-box\",\n      pointerEvents: \"none\",\n      borderRadius: cs.borderRadius,\n      background: cs.backgroundColor,\n      boxShadow: cs.boxShadow,\n      border: `${cs.borderTopWidth} ${cs.borderTopStyle} ${cs.borderTopColor}`,\n      ...inside(host, toRect),\n    })\n    // The pill paints under the labels: the host becomes its own layer for the length of the flight.\n    const undo: (() => void)[] = []\n    const set = (prop: \"position\" | \"isolation\", value: string) => {\n      if (getComputedStyle(host)[prop] !== (prop === \"position\" ? \"static\" : \"auto\")) return\n      host.style[prop] = value\n      undo.push(() => host.style.removeProperty(prop))\n    }\n    set(\"position\", \"relative\")\n    set(\"isolation\", \"isolate\")\n    host.prepend(pill)\n\n    // The item keeps its label, in the colour it has at rest, until the pill arrives under it.\n    const bare = { backgroundColor: \"transparent\", boxShadow: \"none\", borderColor: \"transparent\", color: inactiveColor }\n    const anims = [\n      to.animate([bare, bare], { duration: ms }),\n      pill.animate([inside(host, from), inside(host, toRect)], { duration: ms, easing: easingOf(to) }),\n    ]\n    flights.set(host, { pill, anims, restore: () => undo.forEach((u) => u()) })\n    anims[1].finished.then(\n      () => land(host),\n      () => {},\n    )\n    emitCut({ cut: \"highlight\", component: (host.getAttribute(\"data-slot\") ?? \"highlight\").replace(/-content$/, \"\"), phase: \"enter\", ms: Math.round(ms) })\n  }\n\n  const settle = (host: HTMLElement, row: Row) => {\n    const before = last.get(host) ?? null\n    const now = activeIn(host, row)\n    const after = now.length === 1 ? (now[0] as HTMLElement) : null\n    last.set(host, after)\n    if (!after || before === after) return\n    const recent = pressed && pressed.row === row && performance.now() - pressed.t < 800 ? pressed : null\n    // A brand-new host with nothing pressed before it is a first render, not a change.\n    if (!before && !recent) return\n    const flying = flights.get(host)\n    const from = flying ? flying.pill.getBoundingClientRect() : before?.isConnected ? before.getBoundingClientRect() : recent?.rect\n    pressed = null\n    land(host)\n    if (!from || optedOut(after) || responsesOff()) return\n    const p = press.last(800)\n    // Keyboard and programmatic changes stay instant; only a pointer press inside this host glides.\n    // (The pressed item may have been rebuilt too; the record taken at the press says it was in here.)\n    if (!p || p.kind !== \"pointer\" || !(recent || host.contains(p.el))) return\n    const to = after.getBoundingClientRect()\n    if (Math.abs(to.left - from.left) < 1 && Math.abs(to.top - from.top) < 1) return\n    if (reducedMotion()) {\n      after.animate([{ opacity: 0.4 }, { opacity: 1 }], { duration: scaled(120, after) })\n      return\n    }\n    const sibling = [...host.querySelectorAll(row.item)].find((el) => el !== after && !el.matches(row.active))\n    glide(host, from, after, sibling ? getComputedStyle(sibling).color : getComputedStyle(after).color)\n  }\n\n  return {\n    match: ITEMS,\n    init: recordAll,\n    added: (el) => {\n      const row = rowOf(el)\n      const host = row && (el.closest(row.host) as HTMLElement | null)\n      if (!row || !host) return\n      // Known host: its items were rebuilt. New host: maybe the whole component was rebuilt on select.\n      dirty.add(host)\n    },\n    changed: (el, attribute) => {\n      if (!ATTRIBUTES.has(attribute)) return\n      const row = rowOf(el)\n      const host = row && (el.closest(row.host) as HTMLElement | null)\n      if (host) dirty.add(host)\n    },\n    flush: () => {\n      for (const host of dirty) {\n        const row = HIGHLIGHTS.find((r) => host.matches(r.host))\n        if (row) settle(host, row)\n      }\n      dirty.clear()\n    },\n    dispose: unsubscribe,\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/highlight.ts"
    },
    {
      "path": "lib/keyframery/adapters/error.ts",
      "content": "/** Error: when a field turns invalid after a submit or after leaving it, its box gives one short shake,\n *  like a rejected password, and the error message rises in. Never while someone is typing in it. */\n\nimport { emitCut, reducedMotion, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport type { PressTracker } from \"../press\"\nimport { optedOut } from \"../state\"\nimport { responsesOff } from \"./toggle\"\n\nconst INVALID = '[aria-invalid=\"true\"],[data-invalid=\"true\"]'\nconst MESSAGE = '[data-slot=\"field-error\"]'\n// Shake the box people see: an input inside a group shakes the whole group.\nconst BOX = '[data-slot=\"input-group\"],[data-slot=\"input-otp\"],[data-slot=\"native-select-wrapper\"]'\nexport const SHAKE: Keyframe[] = [\n  { translate: \"0 0\" },\n  { translate: \"-3px 0\", offset: 0.2 },\n  { translate: \"3px 0\", offset: 0.45 },\n  { translate: \"-2px 0\", offset: 0.7 },\n  { translate: \"0 0\" },\n]\n\nexport function errorAdapter(press: PressTracker): Adapter {\n  const seen = new WeakMap<Element, boolean>()\n  const shaking = new WeakMap<Element, Animation>()\n  // Fields that are invalid right now. A field that drops the attribute altogether no longer matches this\n  // adapter, so each batch re-checks them here; otherwise its next error would look like no change.\n  const invalid = new Set<Element>()\n  let ready = false\n\n  const remember = (el: Element) => {\n    const now = el.matches(INVALID)\n    seen.set(el, now)\n    if (now) invalid.add(el)\n  }\n\n  /** A submit (pointer or Enter) just happened, or focus has already left the field: blur validation. */\n  const intended = (el: Element) => {\n    const p = press.last(600)\n    if (p && (p.kind === \"pointer\" || p.key === \"Enter\")) return true\n    const focused = document.activeElement\n    return !(focused && (focused === el || el.contains(focused)))\n  }\n\n  const shake = (el: HTMLElement) => {\n    const box = (el.closest(BOX) as HTMLElement | null) ?? el\n    if (shaking.get(box)?.playState === \"running\") return\n    const ms = scaled(240, box)\n    shaking.set(box, box.animate(SHAKE, { duration: ms, easing: \"ease-out\" }))\n    emitCut({ cut: \"error\", component: box.getAttribute(\"data-slot\") ?? box.tagName.toLowerCase(), phase: \"enter\", ms: Math.round(ms) })\n  }\n\n  return {\n    match: `${INVALID},[aria-invalid],[data-invalid],${MESSAGE}`,\n    init: () => {\n      document.querySelectorAll(`[aria-invalid],[data-invalid]`).forEach(remember)\n      ready = true\n    },\n    added: (el) => {\n      if (el.matches(MESSAGE)) {\n        if (!ready || optedOut(el) || responsesOff()) return\n        el.animate(\n          reducedMotion() ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 0, translate: \"0 -4px\" }, { opacity: 1, translate: \"0 0\" }],\n          { duration: scaled(160, el), easing: \"cubic-bezier(0.22, 1, 0.36, 1)\" },\n        )\n        return\n      }\n      remember(el)\n    },\n    changed: (el, attribute) => {\n      if (attribute !== \"aria-invalid\" && attribute !== \"data-invalid\") return\n      const now = el.matches(INVALID)\n      const was = seen.get(el) ?? false\n      seen.set(el, now)\n      if (now) invalid.add(el)\n      else invalid.delete(el)\n      if (!now || was) return\n      // A field-level flag repeats what its control already says; shake the control only.\n      if (el.matches('[data-slot=\"field\"]') && el.querySelector('[aria-invalid=\"true\"]')) return\n      if (optedOut(el) || responsesOff() || reducedMotion() || !intended(el)) return\n      shake(el)\n    },\n    flush: () => {\n      for (const el of invalid) {\n        if (el.isConnected && el.matches(INVALID)) continue\n        invalid.delete(el)\n        seen.set(el, false)\n      }\n    },\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/error.ts"
    },
    {
      "path": "lib/keyframery/adapters/arrival.ts",
      "content": "/** Arrival icons: a toast's status icon and an alert dialog's icon play once as they arrive, a beat after\n *  the cut that brings them in. A success tick and an error cross draw themselves, a warning rocks once,\n *  information drops in, and a loading spinner keeps its own spin. */\n\nimport { emitCut, reducedMotion, scaled } from \"../motion\"\nimport { isOutline, punch, redraw } from \"../icon\"\nimport type { Adapter } from \"../observe\"\nimport { optedOut } from \"../state\"\nimport { responsesOff } from \"./toggle\"\n\nconst HOSTS = '[data-sonner-toast],[data-slot=\"toast\"],[data-slot=\"alert-dialog-media\"]'\n\n/** The status icon: sonner keeps it in [data-icon]; elsewhere it's the first icon outside a button. */\nfunction iconIn(host: Element): SVGElement | null {\n  const sonner = host.querySelector(\"[data-icon] svg\")\n  if (sonner) return sonner as SVGElement\n  return ([...host.querySelectorAll(\"svg\")].find((svg) => !svg.closest(\"button\")) as SVGElement | undefined) ?? null\n}\n\nexport function arrivalAdapter(): Adapter {\n  let ready = false\n  // Base UI inserts a dialog in two steps, so the same icon can be reported as added twice.\n  const played = new WeakSet<Element>()\n\n  const play = (host: HTMLElement) => {\n    if (!ready || played.has(host) || optedOut(host) || responsesOff() || reducedMotion()) return\n    played.add(host)\n    const svg = iconIn(host)\n    if (!svg) return\n    const type = host.closest(\"[data-type]\")?.getAttribute(\"data-type\") ?? (host.matches('[data-slot=\"alert-dialog-media\"]') ? \"media\" : \"\")\n    if (type === \"loading\") return\n    const ms = scaled(320, host)\n    const delay = scaled(120, host)\n    svg.style.transformOrigin = \"50% 50%\"\n    if (type === \"warning\") {\n      svg.style.transformOrigin = \"50% 85%\"\n      svg.animate([{ rotate: \"0deg\" }, { rotate: \"-12deg\" }, { rotate: \"10deg\" }, { rotate: \"-6deg\" }, { rotate: \"0deg\" }], { duration: ms * 1.3, delay, easing: \"ease-out\" })\n    } else if (type === \"info\") {\n      svg.animate([{ translate: \"0 -4px\", opacity: 0 }, { translate: \"0 0\", opacity: 1 }], { duration: ms, delay, easing: \"cubic-bezier(0.22, 1, 0.36, 1)\", fill: \"backwards\" })\n    } else if (isOutline(svg)) redraw(svg, ms, delay)\n    else punch(svg, ms, delay)\n    emitCut({ cut: \"icon\", component: host.matches('[data-slot=\"alert-dialog-media\"]') ? \"alert-dialog\" : \"toast\", phase: \"enter\", ms: Math.round(ms + delay) })\n  }\n\n  return {\n    match: HOSTS,\n    init: () => {\n      ready = true\n    },\n    added: play,\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/arrival.ts"
    },
    {
      "path": "lib/keyframery/adapters/appear.ts",
      "content": "/** Arrivals in place: an alert, a combobox chip, an attachment or an avatar's photo that shows up after\n *  the page has loaded eases in instead of popping. Only when it is itself inserted (`{show && <Alert />}`),\n *  never as part of a whole new page on a route change. An attachment's upload also gets the response it\n *  deserves: a small punch when it's done, the error shake when it fails. */\n\nimport { emitCut, reducedMotion, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport { optedOut } from \"../state\"\nimport { SHAKE } from \"./error\"\nimport { responsesOff } from \"./toggle\"\n\nconst ENTER: Record<string, Keyframe[]> = {\n  alert: [{ opacity: 0, translate: \"0 -6px\" }, { opacity: 1, translate: \"0 0\" }],\n  attachment: [{ opacity: 0, scale: \"0.96\" }, { opacity: 1, scale: \"1\" }],\n  \"combobox-chip\": [{ opacity: 0, scale: \"0.85\" }, { opacity: 1, scale: \"1\" }],\n  \"avatar-image\": [{ opacity: 0 }, { opacity: 1 }],\n}\nconst MATCH = Object.keys(ENTER)\n  .map((s) => `[data-slot=\"${s}\"]`)\n  .join(\",\")\nconst EASE = \"cubic-bezier(0.22, 1, 0.36, 1)\"\n\nexport function appearAdapter(): Adapter {\n  let ready = false\n  const states = new WeakMap<Element, string | null>()\n\n  return {\n    match: MATCH,\n    init: () => {\n      document.querySelectorAll('[data-slot=\"attachment\"]').forEach((el) => states.set(el, el.getAttribute(\"data-state\")))\n      ready = true\n    },\n    added: (el, root) => {\n      const slot = el.getAttribute(\"data-slot\") ?? \"\"\n      if (slot === \"attachment\") states.set(el, el.getAttribute(\"data-state\"))\n      // Part of a bigger insertion (a new page, a remounted list): that arrival isn't this element's change.\n      if (!ready || (root && root !== el) || optedOut(el) || responsesOff()) return\n      const ms = scaled(slot === \"avatar-image\" ? 200 : 220, el)\n      el.animate(reducedMotion() ? [{ opacity: 0 }, { opacity: 1 }] : ENTER[slot], { duration: ms, easing: EASE })\n      emitCut({ cut: \"appear\", component: slot, phase: \"enter\", ms: Math.round(ms) })\n    },\n    changed: (el, attribute) => {\n      if (attribute !== \"data-state\" || !el.matches('[data-slot=\"attachment\"]')) return\n      const now = el.getAttribute(\"data-state\")\n      const was = states.get(el) ?? null\n      states.set(el, now)\n      if (now === was || optedOut(el) || responsesOff()) return\n      // The content crossfades in every case; the shake and the punch are movement, so reduced motion skips them.\n      el.querySelector('[data-slot=\"attachment-content\"]')?.animate([{ opacity: 0.5 }, { opacity: 1 }], { duration: scaled(160, el), easing: EASE })\n      if (!reducedMotion()) {\n        if (now === \"error\") el.animate(SHAKE, { duration: scaled(240, el), easing: \"ease-out\" })\n        else if (now === \"done\" && (was === \"uploading\" || was === \"processing\")) {\n          el.querySelector('[data-slot=\"attachment-media\"]')?.animate([{ scale: \"1\" }, { scale: \"1.08\", offset: 0.4 }, { scale: \"1\" }], { duration: scaled(300, el), easing: \"ease-out\" })\n        }\n      }\n      emitCut({ cut: now === \"error\" ? \"error\" : \"appear\", component: \"attachment\", phase: \"enter\", ms: Math.round(scaled(240, el)) })\n    },\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/appear.ts"
    },
    {
      "path": "lib/keyframery/adapters/calendar.ts",
      "content": "/** Calendar: a new month slides in from the side you went toward (next from the right, previous from the\n *  left), the way tabs switch to a neighbour. A month stepped with the keyboard (arrow keys, Page Down)\n *  changes instantly. The calendar keeps its grid and replaces the week rows, so the rows slide together. */\n\nimport { easingOf, emitCut, reducedMotion, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport type { PressTracker } from \"../press\"\nimport { optedOut } from \"../state\"\nimport { responsesOff } from \"./toggle\"\n\nconst NAV = '[data-slot=\"calendar\"] :is(.rdp-button_previous, .rdp-button_next)'\nconst WEEKS = '[data-slot=\"calendar\"] .rdp-week'\n\nexport function calendarAdapter(press: PressTracker): Adapter {\n  let pending: { dir: 1 | -1; t: number; told: boolean } | null = null\n  const unsubscribe = press.onPress((p) => {\n    const nav = p.el.closest(NAV)\n    if (!nav || (p.kind === \"key\" && p.key !== \"Enter\" && p.key !== \" \")) return\n    pending = { dir: nav.matches(\".rdp-button_next\") ? 1 : -1, t: performance.now(), told: false }\n  })\n\n  return {\n    match: WEEKS,\n    added: (el) => {\n      if (!pending || performance.now() - pending.t > 800) return\n      if (optedOut(el) || responsesOff()) return\n      const ms = scaled(260, el)\n      el.animate(reducedMotion() ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 0, translate: `${pending.dir * 16}px 0` }, { opacity: 1, translate: \"0 0\" }], {\n        duration: ms,\n        easing: easingOf(el),\n      })\n      if (pending.told) return\n      pending.told = true\n      emitCut({ cut: \"j-cut\", component: \"calendar\", phase: \"enter\", ms: Math.round(ms) })\n    },\n    dispose: unsubscribe,\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/calendar.ts"
    },
    {
      "path": "lib/keyframery/adapters/steps.ts",
      "content": "/** Questionnaire: the next question slides in from the right and the previous one from the left, the way\n *  tabs switch to a neighbour. The question you leave hides at once, so focus never waits for motion. */\n\nimport { easingOf, emitCut, reducedMotion, scaled } from \"../motion\"\nimport type { Adapter } from \"../observe\"\nimport { optedOut } from \"../state\"\nimport { responsesOff } from \"./toggle\"\n\nconst ROOT = '[data-slot=\"questionnaire\"]'\nconst ITEM = '[data-slot=\"questionnaire-item\"]'\n\nexport function stepsAdapter(): Adapter {\n  const current = new WeakMap<Element, Element>()\n\n  return {\n    match: ITEM,\n    init: () =>\n      document.querySelectorAll(ROOT).forEach((root) => {\n        const active = root.querySelector(`${ITEM}[data-active]`)\n        if (active) current.set(root, active)\n      }),\n    changed: (el, attribute) => {\n      if (attribute !== \"data-active\" || !el.hasAttribute(\"data-active\")) return\n      const root = el.closest(ROOT)\n      if (!root) return\n      const before = current.get(root)\n      current.set(root, el)\n      if (!before || before === el || optedOut(el) || responsesOff()) return\n      const items = [...root.querySelectorAll(ITEM)]\n      const dir = items.indexOf(el) > items.indexOf(before) ? 1 : -1\n      const ms = scaled(260, el)\n      el.animate(reducedMotion() ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 0, translate: `${dir * 16}px 0` }, { opacity: 1, translate: \"0 0\" }], {\n        duration: ms,\n        easing: easingOf(el),\n      })\n      emitCut({ cut: \"j-cut\", component: \"questionnaire\", phase: \"enter\", ms: Math.round(ms) })\n    },\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/adapters/steps.ts"
    },
    {
      "path": "lib/keyframery/icon.ts",
      "content": "/** Motion on an icon's own strokes, shared by the state icons in <Cuts /> and by <IconMoves />. */\n\nexport const STROKES = \"path,circle,rect,line,polyline,polygon,ellipse\"\n\nexport function strokesOf(svg: Element): SVGGeometryElement[] {\n  return [...svg.querySelectorAll<SVGGeometryElement>(STROKES)]\n}\n\n/** Outline icons (lucide, tabler) can redraw their strokes; filled ones (phosphor's fill weight) can't. */\nexport function isOutline(svg: Element): boolean {\n  return (svg.getAttribute(\"fill\") ?? getComputedStyle(svg).fill) === \"none\"\n}\n\n/** One stroke draws itself. A pathLength of 1 lets a single dash cover any shape exactly. */\nexport function draw(el: SVGGeometryElement, duration: number, delay = 0, easing = \"cubic-bezier(0.65, 0, 0.35, 1)\"): Animation {\n  el.setAttribute(\"pathLength\", \"1\")\n  el.style.strokeDasharray = \"1\"\n  const anim = el.animate([{ strokeDashoffset: 1 }, { strokeDashoffset: 0 }], { duration, delay, easing, fill: \"backwards\" })\n  const clean = () => {\n    el.style.strokeDasharray = \"\"\n    el.removeAttribute(\"pathLength\")\n  }\n  anim.finished.then(clean, clean)\n  return anim\n}\n\n/** Every stroke of an icon redraws, one after another. */\nexport function redraw(svg: Element, duration: number, delay = 0): Animation[] {\n  return strokesOf(svg).map((part, i) => draw(part, duration, delay + i * (duration / 6)))\n}\n\n/** A short scale punch, for icons that can't redraw. */\nexport function punch(svg: Element, duration: number, delay = 0): Animation {\n  return svg.animate([{ scale: \"1\" }, { scale: \"1.2\", offset: 0.4 }, { scale: \"1\" }], { duration, delay, easing: \"ease-out\" })\n}\n",
      "type": "registry:lib",
      "target": "lib/keyframery/icon.ts"
    }
  ],
  "type": "registry:component"
}