{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "curriculum",
  "title": "Curriculum",
  "description": "Course-layout container for a sequence of lessons or modules with title, progress, optional intro, and a stack of lesson rows.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/curriculum/curriculum.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\n\nimport {\n  BookOpen,\n  CheckCircle2,\n  ChevronDown,\n  Clock,\n  GraduationCap,\n  Link2,\n  Lock,\n  PlayCircle,\n} from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\nexport type LessonStatus = \"available\" | \"completed\" | \"in-progress\" | \"locked\";\nexport type LessonDifficulty = \"advanced\" | \"beginner\" | \"intermediate\";\n\ntype CurriculumContextValue = {\n  expandedModules: Set<string>;\n  toggleModule: (id: string) => void;\n};\n\nconst CurriculumContext = createContext<CurriculumContextValue | null>(null);\n\nfunction useCurriculumContext(): CurriculumContextValue {\n  const ctx = useContext(CurriculumContext);\n  if (!ctx) {\n    throw new Error(\"CurriculumModule must be used within a Curriculum\");\n  }\n  return ctx;\n}\n\ntype LessonProgressSummary = {\n  completed: number;\n  total: number;\n};\n\ntype LessonElementProps = {\n  children?: ReactNode;\n  status?: LessonStatus;\n};\n\nfunction summarizeLessonProgress(children: ReactNode): LessonProgressSummary {\n  let completed = 0;\n  let total = 0;\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement<LessonElementProps>(child)) {\n      return;\n    }\n\n    if (child.type === CurriculumLesson) {\n      total += 1;\n      if (child.props.status === \"completed\") {\n        completed += 1;\n      }\n      return;\n    }\n\n    const nested = summarizeLessonProgress(child.props.children);\n    total += nested.total;\n    completed += nested.completed;\n  });\n\n  return { completed, total };\n}\n\nfunction statusLabel(status: LessonStatus): string {\n  if (status === \"completed\") return \"Completed\";\n  if (status === \"in-progress\") return \"In progress\";\n  if (status === \"locked\") return \"Locked\";\n  return \"Available\";\n}\n\nfunction statusIcon(status: LessonStatus): ReactNode {\n  if (status === \"completed\") {\n    return <CheckCircle2 className=\"size-4 flex-shrink-0 text-green-500\" />;\n  }\n  if (status === \"in-progress\") {\n    return <PlayCircle className=\"size-4 flex-shrink-0 text-primary\" />;\n  }\n  if (status === \"locked\") {\n    return <Lock className=\"size-4 flex-shrink-0 text-muted-foreground\" />;\n  }\n  return <BookOpen className=\"size-4 flex-shrink-0 text-muted-foreground\" />;\n}\n\nconst difficultyStyles: Record<LessonDifficulty, string> = {\n  advanced: \"bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400\",\n  beginner:\n    \"bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400\",\n  intermediate:\n    \"bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400\",\n};\n\ntype ProgressBarProps = {\n  completed: number;\n  progressPct: number;\n  total: number;\n};\n\nfunction ProgressBar({\n  completed,\n  progressPct,\n  total,\n}: ProgressBarProps): React.ReactNode {\n  if (total === 0) return null;\n  return (\n    <div className=\"mt-1 flex items-center gap-2\">\n      <div className=\"h-1.5 max-w-[120px] flex-1 overflow-hidden rounded-full bg-muted\">\n        <div\n          className=\"h-full rounded-full bg-primary transition-all\"\n          style={{ width: `${progressPct}%` }}\n        />\n      </div>\n      <span className=\"text-xs text-muted-foreground\">\n        {completed}/{total}\n      </span>\n    </div>\n  );\n}\n\ntype LessonMetaProps = {\n  difficulty?: LessonDifficulty;\n  duration?: string;\n  prerequisites?: string[];\n};\n\nfunction LessonMeta({\n  difficulty,\n  duration,\n  prerequisites,\n}: LessonMetaProps): React.ReactNode {\n  const prerequisitesLabel = prerequisites?.length\n    ? `Requires: ${prerequisites.join(\", \")}`\n    : null;\n\n  return (\n    <div className=\"flex flex-shrink-0 items-center gap-2\">\n      {prerequisitesLabel ? (\n        <span\n          aria-label={prerequisitesLabel}\n          className=\"flex items-center gap-1 text-xs text-muted-foreground\"\n          title={prerequisitesLabel}\n        >\n          <Link2 aria-hidden=\"true\" className=\"size-3\" />\n        </span>\n      ) : null}\n      {difficulty ? (\n        <span\n          className={cn(\n            \"rounded px-1.5 py-0.5 text-xs font-medium capitalize\",\n            difficultyStyles[difficulty],\n          )}\n        >\n          {difficulty}\n        </span>\n      ) : null}\n      {duration ? (\n        <span className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n          <Clock className=\"size-3\" />\n          {duration}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\ntype ModuleTriggerProps = {\n  completed: number;\n  description?: string;\n  estimatedHours?: number;\n  id: string;\n  isExpanded: boolean;\n  progressPct: number;\n  title: string;\n  toggle: () => void;\n  total: number;\n};\n\nfunction ModuleTrigger({\n  completed,\n  description,\n  estimatedHours,\n  id,\n  isExpanded,\n  progressPct,\n  title,\n  toggle,\n  total,\n}: ModuleTriggerProps): React.ReactNode {\n  return (\n    <button\n      aria-controls={`module-content-${id}`}\n      aria-expanded={isExpanded}\n      className={cn(\n        \"w-full flex items-start justify-between gap-3 rounded-md px-6 py-4 text-left transition-colors\",\n        \"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2\",\n        isExpanded && \"bg-muted/30\",\n      )}\n      onClick={toggle}\n      type=\"button\"\n    >\n      <div className=\"flex min-w-0 flex-col gap-1\">\n        <span className=\"text-sm font-medium\">{title}</span>\n        {description ? (\n          <span className=\"line-clamp-1 text-xs text-muted-foreground\">\n            {description}\n          </span>\n        ) : null}\n        <ProgressBar\n          completed={completed}\n          progressPct={progressPct}\n          total={total}\n        />\n      </div>\n      <div className=\"mt-0.5 flex flex-shrink-0 items-center gap-3\">\n        {estimatedHours === undefined ? null : (\n          <span className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n            <Clock className=\"size-3.5\" />\n            {estimatedHours}h\n          </span>\n        )}\n        <ChevronDown\n          className={cn(\n            \"size-4 text-muted-foreground transition-transform duration-200\",\n            isExpanded && \"rotate-180\",\n          )}\n        />\n      </div>\n    </button>\n  );\n}\n\nexport type CurriculumProps = {\n  children: ReactNode;\n  className?: string;\n  defaultExpandedModules?: string[];\n  title: string;\n  totalHours?: number;\n};\n\nfunction CurriculumRoot({\n  children,\n  className,\n  defaultExpandedModules,\n  title,\n  totalHours,\n}: CurriculumProps): React.ReactNode {\n  const [expandedModules, setExpandedModules] = useState<Set<string>>(\n    () => new Set(defaultExpandedModules ?? []),\n  );\n\n  const toggleModule = useCallback((id: string) => {\n    setExpandedModules((previous) => {\n      const next = new Set(previous);\n      if (next.has(id)) {\n        next.delete(id);\n      } else {\n        next.add(id);\n      }\n      return next;\n    });\n  }, []);\n\n  const contextValue = useMemo(\n    () => ({ expandedModules, toggleModule }),\n    [expandedModules, toggleModule],\n  );\n\n  return (\n    <CurriculumContext.Provider value={contextValue}>\n      <div\n        className={cn(\n          \"my-6 rounded-lg border bg-card text-card-foreground shadow-sm\",\n          className,\n        )}\n      >\n        <div className=\"flex items-center justify-between border-b px-6 py-4\">\n          <div className=\"flex items-center gap-2\">\n            <GraduationCap className=\"size-5 text-primary\" />\n            <h2 className=\"text-lg font-semibold\">{title}</h2>\n          </div>\n          {totalHours === undefined ? null : (\n            <div className=\"flex items-center gap-1 text-sm text-muted-foreground\">\n              <Clock className=\"size-4\" />\n              <span>{totalHours}h total</span>\n            </div>\n          )}\n        </div>\n        <div aria-label={title} className=\"divide-y\">\n          {children}\n        </div>\n      </div>\n    </CurriculumContext.Provider>\n  );\n}\n\nexport type CurriculumModuleProps = {\n  children: ReactNode;\n  className?: string;\n  description?: string;\n  estimatedHours?: number;\n  id: string;\n  title: string;\n};\n\nfunction CurriculumModule({\n  children,\n  className,\n  description,\n  estimatedHours,\n  id,\n  title,\n}: CurriculumModuleProps): React.ReactNode {\n  const { expandedModules, toggleModule } = useCurriculumContext();\n  const isExpanded = expandedModules.has(id);\n  const { completed, total } = useMemo(\n    () => summarizeLessonProgress(children),\n    [children],\n  );\n  const progressPct = total > 0 ? Math.round((completed / total) * 100) : 0;\n\n  return (\n    <section className={className}>\n      <ModuleTrigger\n        completed={completed}\n        description={description}\n        estimatedHours={estimatedHours}\n        id={id}\n        isExpanded={isExpanded}\n        progressPct={progressPct}\n        title={title}\n        toggle={() => {\n          toggleModule(id);\n        }}\n        total={total}\n      />\n      <div\n        aria-hidden={!isExpanded}\n        className={cn(\n          \"overflow-hidden transition-all duration-200\",\n          isExpanded ? \"max-h-[9999px] opacity-100\" : \"max-h-0 opacity-0\",\n        )}\n        hidden={!isExpanded}\n        id={`module-content-${id}`}\n      >\n        <div className=\"divide-y divide-border/50 pb-2\">{children}</div>\n      </div>\n    </section>\n  );\n}\n\nexport type CurriculumLessonProps = {\n  className?: string;\n  difficulty?: LessonDifficulty;\n  duration?: string;\n  href?: string;\n  id?: string;\n  prerequisites?: string[];\n  status?: LessonStatus;\n  title: string;\n};\n\nfunction CurriculumLesson({\n  className,\n  difficulty,\n  duration,\n  href,\n  prerequisites,\n  status = \"available\",\n  title,\n}: CurriculumLessonProps): React.ReactNode {\n  const isLocked = status === \"locked\";\n\n  const inner = (\n    <div\n      aria-disabled={isLocked || undefined}\n      className={cn(\n        \"flex items-center gap-3 px-6 py-3 pl-10 text-sm transition-colors\",\n        isLocked ? \"cursor-not-allowed opacity-60\" : \"\",\n        !isLocked && href ? \"cursor-pointer hover:bg-muted/40\" : \"\",\n        className,\n      )}\n    >\n      {statusIcon(status)}\n      <span className=\"sr-only\">{statusLabel(status)}</span>\n      <span\n        className={cn(\n          \"min-w-0 flex-1 truncate\",\n          status === \"completed\" && \"text-muted-foreground line-through\",\n          status === \"in-progress\" && \"font-medium\",\n        )}\n      >\n        {title}\n        {isLocked ? <span className=\"sr-only\"> (Locked)</span> : null}\n      </span>\n      <LessonMeta\n        difficulty={difficulty}\n        duration={duration}\n        prerequisites={prerequisites}\n      />\n    </div>\n  );\n\n  if (href && !isLocked) {\n    return (\n      <a\n        className=\"block rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2\"\n        href={href}\n      >\n        {inner}\n      </a>\n    );\n  }\n\n  return inner;\n}\n\ntype CurriculumComponent = ((props: CurriculumProps) => React.ReactNode) & {\n  Lesson: typeof CurriculumLesson;\n  Module: typeof CurriculumModule;\n};\n\nconst Curriculum = Object.assign(CurriculumRoot, {\n  Lesson: CurriculumLesson,\n  Module: CurriculumModule,\n}) as CurriculumComponent;\n\nexport { Curriculum, CurriculumLesson, CurriculumModule };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.2.1",
  "stability": "stable"
}
