{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slideshow",
  "title": "Slideshow",
  "description": "Step-through slideshow for presenting content sequentially.",
  "dependencies": [
    "@vllnt/ui@^0.3.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/slideshow/slideshow.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useEffect, useEffectEvent, useState } from \"react\";\n\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport type { HeadingTag } from \"@vllnt/ui\";\nimport { useBodyScrollLock } from \"@vllnt/ui\";\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  /** Heading tag for the sections label inside the table of contents. Defaults to `h3`. */\n  as?: HeadingTag;\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 SlideshowProgress({ progress }: { progress: number }) {\n  return (\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}\n\nfunction SlideshowHeader({\n  currentIndex,\n  isTocOpen,\n  labels,\n  onExit,\n  onToggleToc,\n  sectionCount,\n  sectionTitle,\n  title,\n}: {\n  currentIndex: number;\n  isTocOpen: boolean;\n  labels: Required<SlideshowLabels>;\n  onExit: () => void;\n  onToggleToc: () => void;\n  sectionCount: number;\n  sectionTitle: string;\n  title: string;\n}) {\n  return (\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={isTocOpen ? labels.closeTocLabel : labels.openTocLabel}\n          className=\"flex-shrink-0 p-2 rounded-lg hover:bg-muted transition-colors\"\n          onClick={onToggleToc}\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\">{sectionTitle}</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}/{sectionCount}\n        </span>\n        <button\n          aria-label={labels.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}\n\nfunction SlideshowToc({\n  as: SectionsHeading = \"h3\",\n  completedSections,\n  currentIndex,\n  isOpen,\n  labels,\n  onClose,\n  onNavigate,\n  sections,\n}: {\n  as?: HeadingTag;\n  completedSections: Set<string>;\n  currentIndex: number;\n  isOpen: boolean;\n  labels: Required<SlideshowLabels>;\n  onClose: () => void;\n  onNavigate: (index: number) => void;\n  sections: SlideshowSection[];\n}) {\n  if (!isOpen) return null;\n\n  return (\n    <div\n      className=\"absolute inset-0 z-20 flex animate-in fade-in-0 duration-200\"\n      onClick={onClose}\n      onKeyDown={(event) => {\n        if (event.key === \"Enter\" || event.key === \" \") onClose();\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          <SectionsHeading className=\"font-semibold\">\n            {labels.sectionsLabel}\n          </SectionsHeading>\n          <button\n            aria-label={labels.closeLabel}\n            className=\"p-2 rounded-lg hover:bg-muted transition-colors\"\n            onClick={onClose}\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                  onNavigate(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  );\n}\n\nfunction SlideshowContent({\n  section,\n  source,\n}: {\n  section: SlideshowSection;\n  source: (section: SlideshowSection) => ReactNode;\n}): ReactNode {\n  return source(section);\n}\n\nfunction SlideshowNav({\n  canGoPrevious,\n  isLastSection,\n  labels,\n  onNext,\n  onPrevious,\n}: {\n  canGoPrevious: boolean;\n  isLastSection: boolean;\n  labels: Required<SlideshowLabels>;\n  onNext: () => void;\n  onPrevious: () => void;\n}) {\n  return (\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={onPrevious}\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>{labels.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={onNext}\n        type=\"button\"\n      >\n        <span>{isLastSection ? labels.finishLabel : labels.nextLabel}</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}\n\nfunction SlideshowImpl({\n  as: SectionsHeading = \"h3\",\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  useBodyScrollLock(true);\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  const onKeyDown = useEffectEvent((event: KeyboardEvent) => {\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\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent): void => {\n      onKeyDown(event);\n    };\n    document.addEventListener(\"keydown\", handleKeyDown, true);\n    return () => {\n      document.removeEventListener(\"keydown\", handleKeyDown, true);\n    };\n  }, []);\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      <SlideshowProgress progress={progress} />\n\n      <SlideshowHeader\n        currentIndex={currentIndex}\n        isTocOpen={isTocOpen}\n        labels={mergedLabels}\n        onExit={onExit}\n        onToggleToc={() => {\n          setIsTocOpen((p) => !p);\n        }}\n        sectionCount={sections.length}\n        sectionTitle={currentSection.title}\n        title={title}\n      />\n\n      <div className=\"relative flex-1 overflow-hidden\">\n        <SlideshowToc\n          as={SectionsHeading}\n          completedSections={completedSections}\n          currentIndex={currentIndex}\n          isOpen={isTocOpen}\n          labels={mergedLabels}\n          onClose={() => {\n            setIsTocOpen(false);\n          }}\n          onNavigate={handleTocNavigate}\n          sections={sections}\n        />\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              <SlideshowContent\n                section={currentSection}\n                source={renderContent}\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <SlideshowNav\n        canGoPrevious={canGoPrevious}\n        isLastSection={isLastSection}\n        labels={mergedLabels}\n        onNext={handleNext}\n        onPrevious={handlePrevious}\n      />\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"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
