{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "content-intro",
  "type": "registry:component",
  "title": "Content Intro",
  "description": "Introductory section for content pages with title and description.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/content-intro/content-intro.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useEffect } from \"react\";\n\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Button } from \"@vllnt/ui\";\n\nexport type ContentIntroSection = {\n  id: string;\n  title: string;\n};\n\nexport type ContentIntroLabels = {\n  continueLabel?: string;\n  startLabel?: string;\n  tableOfContentsLabel?: string;\n};\n\nexport type ContentIntroProps = {\n  /** Extra content sections (share, profile, etc.) */\n  additionalContent?: ReactNode;\n  /** Completed section IDs */\n  completedSections: Set<string>;\n  /** Estimated time to complete */\n  estimatedTime: string;\n  /** Is loading progress */\n  isLoading?: boolean;\n  /** Labels for i18n */\n  labels?: ContentIntroLabels;\n  /** Callback when navigating to section */\n  onGoToSection: (index: number) => void;\n  /** Callback when starting */\n  onStart: () => void;\n  /** Render function for intro content */\n  renderIntroContent: () => ReactNode;\n  /** Sections for TOC */\n  sections: ContentIntroSection[];\n  /** Intro section title */\n  title: string;\n};\n\nconst DEFAULT_LABELS: Required<ContentIntroLabels> = {\n  continueLabel: \"Continue Tutorial\",\n  startLabel: \"Start Tutorial\",\n  tableOfContentsLabel: \"Table of Contents\",\n};\n\nconst EMPTY_CONTENT_INTRO_LABELS: ContentIntroLabels = {};\n\n// eslint-disable-next-line max-lines-per-function -- Complex intro with TOC and sticky button\nfunction ContentIntroImpl({\n  additionalContent,\n  completedSections,\n  estimatedTime,\n  isLoading = false,\n  labels = EMPTY_CONTENT_INTRO_LABELS,\n  onGoToSection,\n  onStart,\n  renderIntroContent,\n  sections,\n  title,\n}: ContentIntroProps): React.ReactNode {\n  const mergedLabels = { ...DEFAULT_LABELS, ...labels };\n  const hasProgress = completedSections.size > 0;\n\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent) => {\n      if (event.key === \"Enter\") {\n        event.preventDefault();\n        onStart();\n      }\n    },\n    [onStart],\n  );\n\n  useEffect(() => {\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => {\n      document.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [handleKeyDown]);\n\n  return (\n    <>\n      <div className=\"animate-in fade-in-0 duration-500 pb-24\">\n        {/* Introduction Content */}\n        <section className=\"py-6\">\n          <h2 className=\"text-2xl md:text-3xl font-semibold mb-6\">{title}</h2>\n          <div className={cn(\"max-w-none\", \"[&_h2:first-of-type]:hidden\")}>\n            {renderIntroContent()}\n          </div>\n        </section>\n\n        {/* Table of Contents */}\n        <section className=\"mt-8 py-6 border-t border-border\">\n          <h3 className=\"text-lg font-semibold mb-4\">\n            {mergedLabels.tableOfContentsLabel}\n          </h3>\n          <ol className=\"space-y-2\">\n            {sections.map((section, index) => {\n              const isCompleted =\n                !isLoading && completedSections.has(section.id);\n              return (\n                <li key={section.id}>\n                  <button\n                    className=\"w-full flex items-center gap-3 p-2 -m-2 rounded-lg hover:bg-muted/50 transition-colors text-left\"\n                    onClick={() => {\n                      onGoToSection(index);\n                    }}\n                    type=\"button\"\n                  >\n                    <span\n                      className={cn(\n                        \"flex-shrink-0 size-6 rounded-full flex items-center justify-center text-xs font-medium tabular-nums transition-colors\",\n                        isLoading && \"animate-pulse bg-muted\",\n                        !isLoading &&\n                          isCompleted &&\n                          \"bg-foreground text-background\",\n                        !isLoading && !isCompleted && \"bg-muted\",\n                      )}\n                    >\n                      {isCompleted ? (\n                        <svg\n                          className=\"size-3\"\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                      ) : (\n                        index + 1\n                      )}\n                    </span>\n                    <span\n                      className={cn(\n                        \"text-sm\",\n                        isCompleted && \"line-through text-muted-foreground\",\n                      )}\n                    >\n                      {section.title}\n                    </span>\n                  </button>\n                </li>\n              );\n            })}\n          </ol>\n        </section>\n\n        {/* Extra Content (Share, Profile, etc.) */}\n        {additionalContent}\n      </div>\n\n      {/* Sticky Start/Continue Button */}\n      <div className=\"fixed bottom-0 left-0 right-0 z-50 border-t border-border bg-background/80 backdrop-blur-sm safe-bottom\">\n        <div className=\"mx-auto max-w-3xl p-4\">\n          <div className=\"flex items-center justify-between gap-4\">\n            <p className=\"text-sm text-muted-foreground hidden sm:block\">\n              {hasProgress\n                ? `${completedSections.size}/${sections.length} completed`\n                : `${sections.length} sections · ${estimatedTime}`}\n            </p>\n            <Button\n              className=\"flex-1 sm:flex-none px-8 py-6 text-lg font-medium gap-2\"\n              onClick={onStart}\n              size=\"lg\"\n            >\n              <svg\n                className=\"size-5\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                viewBox=\"0 0 24 24\"\n              >\n                <path\n                  d=\"M5 3l14 9-14 9V3z\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  strokeWidth={2}\n                />\n              </svg>\n              <span>\n                {hasProgress\n                  ? mergedLabels.continueLabel\n                  : mergedLabels.startLabel}\n              </span>\n              <kbd className=\"hidden md:inline-flex ml-1 px-1.5 py-0.5 text-xs font-mono bg-primary-foreground/20 rounded\">\n                ↵\n              </kbd>\n            </Button>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n}\n\nexport const ContentIntro = memo(ContentIntroImpl);\nContentIntro.displayName = \"ContentIntro\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
