{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slideshow",
  "type": "registry:component",
  "title": "Slideshow",
  "description": "Step-through slideshow for presenting content sequentially.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/slideshow/slideshow.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useEffect, useState } from \"react\";\n\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { useMounted } from \"@vllnt/ui\";\nimport { cn } from \"@vllnt/ui\";\nimport { CompletionDialog } from \"@vllnt/ui\";\n\nexport type SlideshowSection = {\n  id: string;\n  title: string;\n};\n\nexport type SlideshowLabels = {\n  closeLabel?: string;\n  closeTocLabel?: string;\n  exitLabel?: string;\n  finishLabel?: string;\n  nextLabel?: string;\n  openTocLabel?: string;\n  prevLabel?: string;\n  sectionsLabel?: string;\n};\n\nexport type SlideshowProps = {\n  /** Completed section IDs */\n  completedSections: Set<string>;\n  /** Dialog labels */\n  completionDialogTitle?: string;\n  /** Current section index */\n  currentIndex: number;\n  /** Labels for i18n */\n  labels?: SlideshowLabels;\n  /** Callback when tutorial completes */\n  onComplete: () => void;\n  /** Callback to exit slideshow */\n  onExit: () => void;\n  /** Callback to navigate to section */\n  onNavigate: (index: number) => void;\n  /** Callback to toggle section completion */\n  onToggleSection: (sectionId: string) => void;\n  /** Render function for section content */\n  renderContent: (section: SlideshowSection) => ReactNode;\n  /** Sections to display */\n  sections: SlideshowSection[];\n  /** Tutorial title */\n  title: string;\n};\n\nconst DEFAULT_LABELS: Required<SlideshowLabels> = {\n  closeLabel: \"Close\",\n  closeTocLabel: \"Close table of contents\",\n  exitLabel: \"Exit\",\n  finishLabel: \"Finish\",\n  nextLabel: \"Next\",\n  openTocLabel: \"Open table of contents\",\n  prevLabel: \"Prev\",\n  sectionsLabel: \"Sections\",\n};\n\nconst EMPTY_SLIDESHOW_LABELS: SlideshowLabels = {};\n\nfunction SlideshowImpl({\n  completedSections,\n  completionDialogTitle = \"Mark section as complete?\",\n  currentIndex,\n  labels = EMPTY_SLIDESHOW_LABELS,\n  onComplete,\n  onExit,\n  onNavigate,\n  onToggleSection,\n  renderContent,\n  sections,\n  title,\n}: SlideshowProps): React.ReactNode {\n  const mergedLabels = { ...DEFAULT_LABELS, ...labels };\n  const [animationDirection, setAnimationDirection] = useState<\n    \"left\" | \"right\" | null\n  >(null);\n  const [isCompletionDialogOpen, setIsCompletionDialogOpen] = useState(false);\n  const [isTocOpen, setIsTocOpen] = useState(false);\n  const mounted = useMounted();\n\n  const currentSection = sections[currentIndex];\n  const isCurrentCompleted = currentSection\n    ? completedSections.has(currentSection.id)\n    : false;\n  const isLastSection = currentIndex === sections.length - 1;\n  const canGoNext = currentIndex < sections.length - 1;\n  const canGoPrevious = currentIndex > 0;\n  const progress = ((currentIndex + 1) / sections.length) * 100;\n\n  useEffect(() => {\n    document.body.style.overflow = \"hidden\";\n    return () => {\n      document.body.style.overflow = \"\";\n    };\n  }, []);\n\n  const goToSection = useCallback(\n    (index: number, direction: \"left\" | \"right\") => {\n      setAnimationDirection(direction);\n      setTimeout(() => {\n        onNavigate(index);\n        setAnimationDirection(null);\n      }, 150);\n    },\n    [onNavigate],\n  );\n\n  const handlePrevious = useCallback(() => {\n    if (canGoPrevious) goToSection(currentIndex - 1, \"right\");\n  }, [canGoPrevious, currentIndex, goToSection]);\n\n  const handleNext = useCallback(() => {\n    if (!canGoNext) {\n      if (isCurrentCompleted) onComplete();\n      else setIsCompletionDialogOpen(true);\n      return;\n    }\n    if (isCurrentCompleted) goToSection(currentIndex + 1, \"left\");\n    else setIsCompletionDialogOpen(true);\n  }, [canGoNext, currentIndex, goToSection, isCurrentCompleted, onComplete]);\n\n  const handleMarkComplete = useCallback(() => {\n    if (currentSection) onToggleSection(currentSection.id);\n    setIsCompletionDialogOpen(false);\n    if (isLastSection) onComplete();\n    else goToSection(currentIndex + 1, \"left\");\n  }, [\n    currentSection,\n    onToggleSection,\n    isLastSection,\n    onComplete,\n    goToSection,\n    currentIndex,\n  ]);\n\n  const handleSkip = useCallback(() => {\n    setIsCompletionDialogOpen(false);\n    if (isLastSection) onComplete();\n    else goToSection(currentIndex + 1, \"left\");\n  }, [isLastSection, onComplete, goToSection, currentIndex]);\n\n  const handleTocNavigate = useCallback(\n    (index: number) => {\n      setIsTocOpen(false);\n      goToSection(index, index > currentIndex ? \"left\" : \"right\");\n    },\n    [currentIndex, goToSection],\n  );\n\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent): void => {\n      if (isCompletionDialogOpen) return;\n      if (event.key === \"Escape\") {\n        event.preventDefault();\n        if (isTocOpen) setIsTocOpen(false);\n        else onExit();\n        return;\n      }\n      if (event.key === \"t\" || event.key === \"T\") {\n        event.preventDefault();\n        setIsTocOpen((p) => !p);\n        return;\n      }\n      if (event.key === \"ArrowRight\" || event.key === \"j\") {\n        event.preventDefault();\n        handleNext();\n        return;\n      }\n      if (event.key === \"ArrowLeft\" || event.key === \"k\") {\n        event.preventDefault();\n        handlePrevious();\n      }\n    };\n    document.addEventListener(\"keydown\", handleKeyDown, true);\n    return () => {\n      document.removeEventListener(\"keydown\", handleKeyDown, true);\n    };\n  }, [handleNext, handlePrevious, onExit, isTocOpen, isCompletionDialogOpen]);\n\n  if (!currentSection || !mounted) return null;\n\n  return createPortal(\n    <div className=\"fixed inset-0 z-[9999] bg-background flex flex-col\">\n      {/* Progress Bar */}\n      <div className=\"absolute top-0 left-0 right-0 h-1 bg-muted z-10\">\n        <div\n          className=\"h-full bg-foreground transition-all duration-300 ease-out\"\n          style={{ width: `${progress}%` }}\n        />\n      </div>\n\n      {/* Header */}\n      <div className=\"flex items-center justify-between px-4 py-3 mt-1 border-b border-border bg-background\">\n        <div className=\"flex items-center gap-3 min-w-0 flex-1\">\n          <button\n            aria-label={\n              isTocOpen ? mergedLabels.closeTocLabel : mergedLabels.openTocLabel\n            }\n            className=\"flex-shrink-0 p-2 rounded-lg hover:bg-muted transition-colors\"\n            onClick={() => {\n              setIsTocOpen((p) => !p);\n            }}\n            type=\"button\"\n          >\n            {isTocOpen ? (\n              <svg\n                className=\"size-5\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                viewBox=\"0 0 24 24\"\n              >\n                <path\n                  d=\"M6 18L18 6M6 6l12 12\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  strokeWidth={2}\n                />\n              </svg>\n            ) : (\n              <svg\n                className=\"size-5\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                viewBox=\"0 0 24 24\"\n              >\n                <path\n                  d=\"M4 6h16M4 12h16M4 18h16\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  strokeWidth={2}\n                />\n              </svg>\n            )}\n          </button>\n          <div className=\"min-w-0 flex-1\">\n            <p className=\"text-xs text-muted-foreground truncate\">{title}</p>\n            <p className=\"text-sm font-medium truncate\">\n              {currentSection.title}\n            </p>\n          </div>\n        </div>\n        <div className=\"flex items-center gap-2 flex-shrink-0\">\n          <span className=\"text-xs text-muted-foreground tabular-nums hidden sm:inline\">\n            {currentIndex + 1}/{sections.length}\n          </span>\n          <button\n            aria-label={mergedLabels.exitLabel}\n            className=\"p-2 rounded-lg hover:bg-muted transition-colors\"\n            onClick={onExit}\n            type=\"button\"\n          >\n            <svg\n              className=\"size-5\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              viewBox=\"0 0 24 24\"\n            >\n              <path\n                d=\"M6 18L18 6M6 6l12 12\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth={2}\n              />\n            </svg>\n          </button>\n        </div>\n      </div>\n\n      {/* Content */}\n      <div className=\"relative flex-1 overflow-hidden\">\n        {isTocOpen ? (\n          <div\n            className=\"absolute inset-0 z-20 flex animate-in fade-in-0 duration-200\"\n            onClick={() => {\n              setIsTocOpen(false);\n            }}\n            onKeyDown={(event) => {\n              if (event.key === \"Enter\" || event.key === \" \")\n                setIsTocOpen(false);\n            }}\n            role=\"button\"\n            tabIndex={0}\n          >\n            <div className=\"absolute inset-0 bg-background/40\" />\n            {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}\n            <div\n              className=\"relative w-full sm:max-w-sm bg-background border-r border-border h-full overflow-auto shadow-2xl\"\n              onClick={(event) => {\n                event.stopPropagation();\n              }}\n              onKeyDown={(event) => {\n                event.stopPropagation();\n              }}\n              role=\"dialog\"\n            >\n              <div className=\"sticky top-0 flex items-center justify-between px-4 py-3 border-b border-border bg-background\">\n                <h3 className=\"font-semibold\">{mergedLabels.sectionsLabel}</h3>\n                <button\n                  aria-label={mergedLabels.closeLabel}\n                  className=\"p-2 rounded-lg hover:bg-muted transition-colors\"\n                  onClick={() => {\n                    setIsTocOpen(false);\n                  }}\n                  type=\"button\"\n                >\n                  <svg\n                    className=\"size-4\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    viewBox=\"0 0 24 24\"\n                  >\n                    <path\n                      d=\"M6 18L18 6M6 6l12 12\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      strokeWidth={2}\n                    />\n                  </svg>\n                </button>\n              </div>\n              <div className=\"p-2\">\n                {sections.map((section, index) => {\n                  const isCompleted = completedSections.has(section.id);\n                  const isCurrent = index === currentIndex;\n                  return (\n                    <button\n                      className={cn(\n                        \"w-full flex items-center gap-3 p-3 rounded-lg text-left transition-colors\",\n                        isCurrent ? \"bg-muted\" : \"hover:bg-muted/50\",\n                      )}\n                      key={section.id}\n                      onClick={() => {\n                        handleTocNavigate(index);\n                      }}\n                      type=\"button\"\n                    >\n                      <div\n                        className={cn(\n                          \"flex-shrink-0 size-5 rounded-full border-2 flex items-center justify-center\",\n                          isCompleted\n                            ? \"bg-foreground border-foreground\"\n                            : \"border-muted-foreground\",\n                        )}\n                      >\n                        {isCompleted ? (\n                          <svg\n                            className=\"size-3 text-background\"\n                            fill=\"none\"\n                            stroke=\"currentColor\"\n                            viewBox=\"0 0 24 24\"\n                          >\n                            <path\n                              d=\"M5 13l4 4L19 7\"\n                              strokeLinecap=\"round\"\n                              strokeLinejoin=\"round\"\n                              strokeWidth={2}\n                            />\n                          </svg>\n                        ) : null}\n                      </div>\n                      <span\n                        className={cn(\n                          \"flex-1 text-sm truncate\",\n                          isCompleted && \"line-through opacity-60\",\n                        )}\n                      >\n                        {section.title}\n                      </span>\n                    </button>\n                  );\n                })}\n              </div>\n            </div>\n          </div>\n        ) : null}\n\n        <div className=\"h-full overflow-auto px-4 py-8 md:px-8 lg:px-16\">\n          <div className=\"mx-auto max-w-3xl\">\n            <div\n              className={cn(\n                \"transition-all duration-150 ease-out\",\n                animationDirection === \"left\" && \"opacity-0 -translate-x-4\",\n                animationDirection === \"right\" && \"opacity-0 translate-x-4\",\n                !animationDirection && \"opacity-100 translate-x-0\",\n              )}\n            >\n              {renderContent(currentSection)}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Bottom Nav */}\n      <div className=\"relative z-20 flex items-center justify-between p-4 border-t border-border bg-background\">\n        <button\n          className=\"min-w-[100px] gap-1 inline-flex items-center justify-center px-4 py-2 rounded-md hover:bg-muted transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n          disabled={!canGoPrevious}\n          onClick={handlePrevious}\n          type=\"button\"\n        >\n          <svg\n            className=\"size-4\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            viewBox=\"0 0 24 24\"\n          >\n            <path\n              d=\"m15 19-7-7 7-7\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth={2}\n            />\n          </svg>\n          <span>{mergedLabels.prevLabel}</span>\n        </button>\n        <button\n          className=\"min-w-[100px] gap-1 inline-flex items-center justify-center px-4 py-2 rounded-md bg-foreground text-background hover:bg-foreground/90 transition-colors\"\n          onClick={handleNext}\n          type=\"button\"\n        >\n          <span>\n            {isLastSection ? mergedLabels.finishLabel : mergedLabels.nextLabel}\n          </span>\n          {!isLastSection && (\n            <svg\n              className=\"size-4\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              viewBox=\"0 0 24 24\"\n            >\n              <path\n                d=\"m9 5 7 7-7 7\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth={2}\n              />\n            </svg>\n          )}\n        </button>\n      </div>\n\n      <CompletionDialog\n        description={`You're about to ${isLastSection ? \"finish\" : \"move to the next section from\"}: ${currentSection.title}`}\n        isOpen={isCompletionDialogOpen}\n        onCancel={handleSkip}\n        onClose={() => {\n          setIsCompletionDialogOpen(false);\n        }}\n        onConfirm={handleMarkComplete}\n        title={completionDialogTitle}\n      />\n    </div>,\n    document.body,\n  );\n}\n\nexport const Slideshow = memo(SlideshowImpl);\nSlideshow.displayName = \"Slideshow\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
