{
  "name": "presentation-editing-demo",
  "type": "registry:example",
  "dependencies": [
    "@diceui/pptx",
    "@dnd-kit/core",
    "@dnd-kit/modifiers",
    "@dnd-kit/sortable",
    "@dnd-kit/utilities",
    "sonner"
  ],
  "registryDependencies": [
    "presentation",
    "presentation-zoom-select",
    "button",
    "input",
    "tooltip"
  ],
  "files": [
    {
      "path": "examples/presentation-editing-demo.tsx",
      "type": "registry:example",
      "content": "\"use client\";\n\nimport type { PresentationStore } from \"@diceui/pptx\";\nimport type {\n  DragEndEvent,\n  DragStartEvent,\n  DropAnimation,\n} from \"@dnd-kit/core\";\n\nimport {\n  useCreatePresentationStore,\n  useHistory,\n  usePresentation,\n} from \"@diceui/pptx\";\nimport {\n  DndContext,\n  DragOverlay,\n  PointerSensor,\n  closestCenter,\n  defaultDropAnimation,\n  defaultDropAnimationSideEffects,\n  useSensor,\n  useSensors,\n} from \"@dnd-kit/core\";\nimport {\n  restrictToFirstScrollableAncestor,\n  restrictToVerticalAxis,\n} from \"@dnd-kit/modifiers\";\nimport {\n  SortableContext,\n  useSortable,\n  verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Redo2Icon, Undo2Icon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\n\nimport { PresentationZoomSelect } from \"@/registry/bases/radix/components/presentation-zoom-select\";\nimport { Button } from \"@/registry/bases/radix/ui/button\";\nimport { Input } from \"@/registry/bases/radix/ui/input\";\nimport {\n  Presentation,\n  PresentationContent,\n  PresentationError,\n  PresentationLoading,\n  PresentationProvider,\n  PresentationSelection,\n  PresentationSlide,\n  PresentationThumbnailItem,\n  PresentationThumbnailItemNumber,\n  PresentationThumbnailItemPreview,\n  PresentationThumbnailList,\n  PresentationViewport,\n} from \"@/registry/bases/radix/ui/presentation\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/registry/bases/radix/ui/tooltip\";\n\nconst DEMO_DECK_PATH = \"/assets/demo.pptx\";\n\n/**\n * Keeps the source item visible during the drop animation.\n *\n * The default side effect sets its opacity to `0`, delaying the thumbnail\n * item's focus ring from reappearing until roughly 250 ms after pointer release.\n */\nconst DROP_ANIMATION: DropAnimation = {\n  ...defaultDropAnimation,\n  sideEffects: defaultDropAnimationSideEffects({ styles: { active: {} } }),\n};\n\nexport default function PresentationEditingDemo() {\n  const store = useCreatePresentationStore();\n\n  React.useEffect(() => {\n    fetch(DEMO_DECK_PATH)\n      .then((res) => {\n        // fetch resolves on 404, so an unchecked body would reach the parser as\n        // an error page rather than a deck.\n        if (!res.ok) throw new Error(`${DEMO_DECK_PATH}: ${res.status}.`);\n        return res.arrayBuffer();\n      })\n      // Editing and reordering both need the source package retained.\n      .then((buffer) => store.load(buffer, { readOnly: false }))\n      .catch(() => {\n        // Fail silently to avoid blocking the main thread.\n      });\n    // oxlint-disable-next-line react-hooks/exhaustive-deps -- store is a stable ref, intentionally omitted from deps\n  }, []);\n\n  return (\n    <div className=\"flex size-full flex-col overflow-hidden\">\n      <PresentationProvider store={store}>\n        <PresentationToolbar store={store} />\n        <Presentation className=\"min-h-0 flex-1\">\n          <SortableThumbnailList store={store} />\n          <PresentationContent>\n            <PresentationLoading />\n            <PresentationError />\n            <PresentationViewport autoFit autoFitPadding={10}>\n              <PresentationSlide>\n                <PresentationSelection undoRedoShortcuts />\n              </PresentationSlide>\n            </PresentationViewport>\n          </PresentationContent>\n        </Presentation>\n      </PresentationProvider>\n    </div>\n  );\n}\n\nfunction PresentationToolbar({ store }: { store: PresentationStore }) {\n  const id = React.useId();\n  const { status } = usePresentation();\n  const { canUndo, canRedo, undo, redo } = useHistory();\n\n  async function onFileChange(event: React.ChangeEvent<HTMLInputElement>) {\n    const file = event.target.files?.[0];\n    if (!file) return;\n    try {\n      await store.load(file, { readOnly: false });\n    } catch {\n      // Fail silently because `load()` already wrote the failure to `store.error`.\n    }\n  }\n\n  return (\n    <div className=\"flex items-center gap-2 border-b p-1.5\">\n      <label htmlFor={`${id}-file`} className=\"sr-only\">\n        Open .pptx\n      </label>\n      <Input\n        id={`${id}-file`}\n        type=\"file\"\n        accept=\".pptx\"\n        className=\"h-8 max-w-56 py-px text-xs\"\n        onChange={onFileChange}\n      />\n      <span className=\"min-w-0 truncate text-sm text-muted-foreground\">\n        {status === \"ready\"\n          ? \"Drag a shape to move it, or drag a thumbnail to reorder the deck\"\n          : \"Loading sample deck…\"}\n      </span>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <span className=\"ml-auto inline-block w-fit\">\n            <Button\n              aria-label=\"Undo\"\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              disabled={!canUndo}\n              onClick={() => undo()}\n            >\n              <Undo2Icon />\n            </Button>\n          </span>\n        </TooltipTrigger>\n        <TooltipContent>Undo</TooltipContent>\n      </Tooltip>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <span className=\"inline-block w-fit\">\n            <Button\n              aria-label=\"Redo\"\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              disabled={!canRedo}\n              onClick={() =>\n                void redo().catch(() => toast.error(\"Redo failed\"))\n              }\n            >\n              <Redo2Icon />\n            </Button>\n          </span>\n        </TooltipTrigger>\n        <TooltipContent>Redo</TooltipContent>\n      </Tooltip>\n      <PresentationZoomSelect />\n    </div>\n  );\n}\n\nfunction SortableThumbnailList({ store }: { store: PresentationStore }) {\n  const { presentation } = usePresentation();\n  const slideIds = presentation?.slides.map((slide) => slide.id) ?? [];\n\n  /**\n   * Order to paint while a drop is being committed. `store.edit()` is async, so\n   * without this the strip would snap back to the old order for a frame between\n   * the pointer release and the edit landing.\n   */\n  const [pendingIds, setPendingIds] = React.useState<string[] | null>(null);\n  const orderedIds = pendingIds ?? slideIds;\n\n  /** Slide under the pointer, mirrored into the drag overlay. */\n  const [draggedId, setDraggedId] = React.useState<string | null>(null);\n\n  // Pointer only: the list owns ArrowUp/ArrowDown for roving focus, so a\n  // keyboard drag sensor bound to the same keys would fight it.\n  const sensors = useSensors(\n    // A small threshold keeps a plain click selecting the slide instead of\n    // starting a drag.\n    useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n  );\n\n  async function onDragEnd(event: DragEndEvent) {\n    const { active, over } = event;\n    setDraggedId(null);\n    if (!over || active.id === over.id) return;\n\n    const slideId = String(active.id);\n    const toIndex = orderedIds.indexOf(String(over.id));\n    if (toIndex === -1) return;\n\n    const fromIndex = orderedIds.indexOf(slideId);\n    if (fromIndex === -1) return;\n\n    const next = [...orderedIds];\n    next.splice(fromIndex, 1);\n    next.splice(toIndex, 0, slideId);\n    setPendingIds(next);\n\n    try {\n      await store.edit({ type: \"moveSlide\", slideId, toIndex });\n    } finally {\n      // The store is the source of truth again once the edit settles.\n      setPendingIds(null);\n    }\n  }\n\n  return (\n    <DndContext\n      sensors={sensors}\n      collisionDetection={closestCenter}\n      // A transformed child still counts toward its scroll container's overflow,\n      // so an unclamped drag past the last thumbnail grows scrollHeight, which\n      // lets auto-scroll run, which grows the transform again: the strip scrolls\n      // forever. Clamping the drag to the scroll port breaks that loop.\n      modifiers={[restrictToVerticalAxis, restrictToFirstScrollableAncestor]}\n      onDragStart={({ active }: DragStartEvent) =>\n        setDraggedId(String(active.id))\n      }\n      onDragCancel={() => setDraggedId(null)}\n      onDragEnd={onDragEnd}\n    >\n      <SortableContext\n        items={orderedIds}\n        strategy={verticalListSortingStrategy}\n      >\n        <PresentationThumbnailList>\n          {() => (\n            <>\n              {orderedIds.map((slideId) => (\n                <SortableItem key={slideId} slideId={slideId} />\n              ))}\n              {/*\n               * Inside the list so the floating copy can read the list context\n               * it needs to paint a real miniature. It is fixed-positioned, so\n               * the strip's overflow does not clip it.\n               */}\n              <DragOverlay dropAnimation={DROP_ANIMATION}>\n                {draggedId ? (\n                  <PresentationThumbnailItem\n                    decorative\n                    slideId={draggedId}\n                    className=\"h-full cursor-grabbing bg-background shadow-lg\"\n                  >\n                    <PresentationThumbnailItemNumber />\n                    <PresentationThumbnailItemPreview />\n                  </PresentationThumbnailItem>\n                ) : null}\n              </DragOverlay>\n            </>\n          )}\n        </PresentationThumbnailList>\n      </SortableContext>\n    </DndContext>\n  );\n}\n\nfunction SortableItem({ slideId }: { slideId: string }) {\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    transform,\n    transition,\n    isDragging,\n  } = useSortable({\n    id: slideId,\n  });\n\n  // dnd-kit sets role=\"button\" and tabIndex={0}; both would override what the\n  // list needs, replacing the `option` role and putting every thumbnail in the\n  // tab order instead of the one roving tab stop.\n  const { role: _role, tabIndex: _tabIndex, ...dragAttributes } = attributes;\n\n  return (\n    <PresentationThumbnailItem\n      slideId={slideId}\n      ref={setNodeRef}\n      style={{ transform: CSS.Transform.toString(transform), transition }}\n      // The overlay carries the thumbnail during the drag, so what stays in the\n      // list is just the slot it will land in.\n      className={isDragging ? \"opacity-30\" : undefined}\n      onSelect={(event) => {\n        // Pressing a thumbnail focuses it, and focus navigates. Suppress that\n        // while dragging so the deck does not jump mid-gesture.\n        if (isDragging) event.preventDefault();\n      }}\n      {...dragAttributes}\n      {...listeners}\n    >\n      <PresentationThumbnailItemNumber />\n      <PresentationThumbnailItemPreview />\n    </PresentationThumbnailItem>\n  );\n}\n",
      "target": ""
    }
  ]
}