diff --git a/apps/tutorial/next-env.d.ts b/apps/tutorial/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/apps/tutorial/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/tutorial/next.config.ts b/apps/tutorial/next.config.ts
new file mode 100644
index 0000000..b5fe066
--- /dev/null
+++ b/apps/tutorial/next.config.ts
@@ -0,0 +1,8 @@
+import type { NextConfig } from 'next';
+
+const nextConfig: NextConfig = {
+ transpilePackages: ['@tpmjs/ui', '@tpmjs/utils'],
+ reactStrictMode: true,
+};
+
+export default nextConfig;
diff --git a/apps/tutorial/package.json b/apps/tutorial/package.json
new file mode 100644
index 0000000..6e0b386
--- /dev/null
+++ b/apps/tutorial/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@tpmjs/tutorial",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev --port 3001",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint .",
+ "type-check": "tsc --noEmit",
+ "clean": "rm -rf .next .turbo"
+ },
+ "dependencies": {
+ "@tpmjs/ui": "workspace:*",
+ "@tpmjs/utils": "workspace:*",
+ "framer-motion": "^11.15.0",
+ "next": "^16.0.8",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@tpmjs/eslint-config": "workspace:*",
+ "@tpmjs/tailwind-config": "workspace:*",
+ "@tpmjs/tsconfig": "workspace:*",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.2",
+ "@types/react-dom": "^19.0.2",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.39.1",
+ "eslint-config-next": "^16.0.4",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.9.3"
+ }
+}
diff --git a/apps/tutorial/postcss.config.js b/apps/tutorial/postcss.config.js
new file mode 100644
index 0000000..12a703d
--- /dev/null
+++ b/apps/tutorial/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/apps/tutorial/src/app/globals.css b/apps/tutorial/src/app/globals.css
new file mode 100644
index 0000000..0501fbe
--- /dev/null
+++ b/apps/tutorial/src/app/globals.css
@@ -0,0 +1,133 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ /* Dark mode by default for dramatic slideshow */
+ --background: 210 10% 5%;
+ --surface: 210 10% 8%;
+ --surface-elevated: 210 10% 12%;
+ --foreground: 210 10% 90%;
+ --foreground-secondary: 210 8% 60%;
+ --foreground-tertiary: 210 6% 40%;
+ --foreground-muted: 210 6% 30%;
+ --border: 210 10% 20%;
+ --border-strong: 210 12% 30%;
+ --border-subtle: 210 8% 15%;
+
+ /* Accent colors - cyan and purple */
+ --accent-cyan: 186 100% 50%;
+ --accent-purple: 260 80% 60%;
+ --accent-gradient: linear-gradient(135deg, hsl(186 100% 50%) 0%, hsl(260 80% 60%) 100%);
+
+ --primary: 210 10% 98%;
+ --primary-foreground: 210 10% 5%;
+
+ /* Radii and shadows */
+ --radius: 8px;
+ --radius-lg: 12px;
+ }
+
+ * {
+ @apply border-border;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ font-family: var(--font-sans);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ overflow: hidden;
+ }
+
+ html, body, #__next {
+ height: 100%;
+ }
+}
+
+@layer utilities {
+ .text-gradient {
+ background: var(--accent-gradient);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ }
+
+ .bg-gradient-accent {
+ background: var(--accent-gradient);
+ }
+
+ .glow-cyan {
+ box-shadow: 0 0 20px hsl(var(--accent-cyan) / 0.3),
+ 0 0 40px hsl(var(--accent-cyan) / 0.2),
+ 0 0 60px hsl(var(--accent-cyan) / 0.1);
+ }
+
+ .glow-purple {
+ box-shadow: 0 0 20px hsl(var(--accent-purple) / 0.3),
+ 0 0 40px hsl(var(--accent-purple) / 0.2),
+ 0 0 60px hsl(var(--accent-purple) / 0.1);
+ }
+
+ /* Animated background gradient */
+ @keyframes gradient-shift {
+ 0%, 100% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+ }
+
+ .animate-gradient {
+ background-size: 200% 200%;
+ animation: gradient-shift 15s ease infinite;
+ }
+
+ /* Floating animation */
+ @keyframes float {
+ 0%, 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-10px);
+ }
+ }
+
+ .animate-float {
+ animation: float 3s ease-in-out infinite;
+ }
+
+ /* Pulse glow */
+ @keyframes pulse-glow {
+ 0%, 100% {
+ opacity: 0.5;
+ }
+ 50% {
+ opacity: 1;
+ }
+ }
+
+ .animate-pulse-glow {
+ animation: pulse-glow 2s ease-in-out infinite;
+ }
+
+ /* Stagger delays */
+ .delay-100 { animation-delay: 100ms; }
+ .delay-200 { animation-delay: 200ms; }
+ .delay-300 { animation-delay: 300ms; }
+ .delay-400 { animation-delay: 400ms; }
+ .delay-500 { animation-delay: 500ms; }
+
+ /* Motion Preferences */
+ @media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+ }
+}
diff --git a/apps/tutorial/src/app/layout.tsx b/apps/tutorial/src/app/layout.tsx
new file mode 100644
index 0000000..7202a0f
--- /dev/null
+++ b/apps/tutorial/src/app/layout.tsx
@@ -0,0 +1,38 @@
+import type { Metadata } from 'next';
+import { Space_Grotesk, Space_Mono } from 'next/font/google';
+import './globals.css';
+
+const spaceGrotesk = Space_Grotesk({
+ subsets: ['latin'],
+ variable: '--font-sans',
+ display: 'swap',
+});
+
+const spaceMono = Space_Mono({
+ subsets: ['latin'],
+ weight: ['400', '700'],
+ variable: '--font-mono',
+ display: 'swap',
+});
+
+export const metadata: Metadata = {
+ title: 'TPMJS Tutorial - Learn How It Works',
+ description: 'Interactive tutorial explaining TPMJS - the Tool Package Manager for AI Agents',
+ openGraph: {
+ title: 'TPMJS Tutorial',
+ description: 'Interactive tutorial explaining TPMJS - the Tool Package Manager for AI Agents',
+ type: 'website',
+ },
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}): React.ReactElement {
+ return (
+
+
{children}
+
+ );
+}
diff --git a/apps/tutorial/src/app/page.tsx b/apps/tutorial/src/app/page.tsx
new file mode 100644
index 0000000..4792ff8
--- /dev/null
+++ b/apps/tutorial/src/app/page.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import { Slideshow } from '@/components/Slideshow';
+
+export default function TutorialPage(): React.ReactElement {
+ return (
+
+
+
+ );
+}
diff --git a/apps/tutorial/src/components/Slide.tsx b/apps/tutorial/src/components/Slide.tsx
new file mode 100644
index 0000000..d9842bf
--- /dev/null
+++ b/apps/tutorial/src/components/Slide.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { type Variants, motion } from 'framer-motion';
+import type { ReactNode } from 'react';
+
+interface SlideProps {
+ children: ReactNode;
+ isActive: boolean;
+ direction: 'left' | 'right' | null;
+}
+
+const variants: Variants = {
+ enter: (direction: 'left' | 'right') => ({
+ x: direction === 'right' ? '100%' : '-100%',
+ opacity: 0,
+ }),
+ center: {
+ x: 0,
+ opacity: 1,
+ },
+ exit: (direction: 'left' | 'right') => ({
+ x: direction === 'right' ? '-100%' : '100%',
+ opacity: 0,
+ }),
+};
+
+export function Slide({ children, isActive, direction }: SlideProps): React.ReactElement | null {
+ if (!isActive) return null;
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/tutorial/src/components/SlideNavigation.tsx b/apps/tutorial/src/components/SlideNavigation.tsx
new file mode 100644
index 0000000..1f5b9bb
--- /dev/null
+++ b/apps/tutorial/src/components/SlideNavigation.tsx
@@ -0,0 +1,69 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+interface SlideNavigationProps {
+ onNext: () => void;
+ onPrev: () => void;
+ isFirst: boolean;
+ isLast: boolean;
+}
+
+export function SlideNavigation({
+ onNext,
+ onPrev,
+ isFirst,
+ isLast,
+}: SlideNavigationProps): React.ReactElement {
+ return (
+ <>
+ {/* Previous button */}
+
+
+
+
+ {/* Next button */}
+
+
+
+ >
+ );
+}
diff --git a/apps/tutorial/src/components/SlideProgress.tsx b/apps/tutorial/src/components/SlideProgress.tsx
new file mode 100644
index 0000000..62c3401
--- /dev/null
+++ b/apps/tutorial/src/components/SlideProgress.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+interface SlideProgressProps {
+ currentSlide: number;
+ totalSlides: number;
+ goTo: (index: number) => void;
+}
+
+export function SlideProgress({
+ currentSlide,
+ totalSlides,
+ goTo,
+}: SlideProgressProps): React.ReactElement {
+ return (
+
+ {Array.from({ length: totalSlides }).map((_, index) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/tutorial/src/components/Slideshow.tsx b/apps/tutorial/src/components/Slideshow.tsx
new file mode 100644
index 0000000..99691a2
--- /dev/null
+++ b/apps/tutorial/src/components/Slideshow.tsx
@@ -0,0 +1,73 @@
+'use client';
+
+import { useSlideshow } from '@/hooks/useSlideshow';
+import { AnimatePresence } from 'framer-motion';
+import { Slide } from './Slide';
+import { SlideNavigation } from './SlideNavigation';
+import { SlideProgress } from './SlideProgress';
+import { DiscoverySlide } from './slides/DiscoverySlide';
+import { GetStartedSlide } from './slides/GetStartedSlide';
+import { HowItWorksSlide } from './slides/HowItWorksSlide';
+import { IntegrationSlide } from './slides/IntegrationSlide';
+import { ProblemSlide } from './slides/ProblemSlide';
+import { QualitySlide } from './slides/QualitySlide';
+import { SolutionSlide } from './slides/SolutionSlide';
+import { ToolDetailSlide } from './slides/ToolDetailSlide';
+import { WelcomeSlide } from './slides/WelcomeSlide';
+
+const SLIDES = [
+ WelcomeSlide,
+ ProblemSlide,
+ SolutionSlide,
+ HowItWorksSlide,
+ DiscoverySlide,
+ IntegrationSlide,
+ QualitySlide,
+ ToolDetailSlide,
+ GetStartedSlide,
+];
+
+export function Slideshow(): React.ReactElement {
+ const { currentSlide, totalSlides, next, prev, goTo, direction, isFirst, isLast } = useSlideshow({
+ totalSlides: SLIDES.length,
+ });
+
+ return (
+
+ {/* Background gradient */}
+
+
+ {/* Subtle grid pattern */}
+
+
+ {/* Slides */}
+
+ {SLIDES.map((SlideContent, index) => (
+
+
+
+ ))}
+
+
+ {/* Navigation */}
+
+
+ {/* Progress */}
+
+
+ {/* Keyboard hint */}
+
+ Use arrow keys or click to navigate
+
+
+ );
+}
diff --git a/apps/tutorial/src/components/slides/DiscoverySlide.tsx b/apps/tutorial/src/components/slides/DiscoverySlide.tsx
new file mode 100644
index 0000000..976d814
--- /dev/null
+++ b/apps/tutorial/src/components/slides/DiscoverySlide.tsx
@@ -0,0 +1,114 @@
+'use client';
+
+import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
+import { useEffect, useState } from 'react';
+
+function AnimatedCounter({ value, delay = 0 }: { value: number; delay?: number }) {
+ const count = useMotionValue(0);
+ const rounded = useTransform(count, (latest) => Math.round(latest));
+ const [displayValue, setDisplayValue] = useState(0);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ const controls = animate(count, value, {
+ duration: 2,
+ ease: 'easeOut',
+ });
+ return () => controls.stop();
+ }, delay * 1000);
+
+ return () => clearTimeout(timeout);
+ }, [count, value, delay]);
+
+ useEffect(() => {
+ const unsubscribe = rounded.on('change', (v) => setDisplayValue(v));
+ return unsubscribe;
+ }, [rounded]);
+
+ return {displayValue};
+}
+
+export function DiscoverySlide(): React.ReactElement {
+ return (
+
+ {/* Background pulse */}
+
+
+
+
+ 🔍
+
+
+ Automatic Discovery
+
+ npm changes feed synced every{' '}
+ 2 minutes
+
+
+ {/* Stats grid */}
+
+
+
+ Tools Registered
+
+
+
+ 2m
+ Sync Interval
+
+
+
+ 24/7
+ Always Running
+
+
+
+ {/* Animated sync indicator */}
+
+
+ Live sync active
+
+
+
+ );
+}
diff --git a/apps/tutorial/src/components/slides/GetStartedSlide.tsx b/apps/tutorial/src/components/slides/GetStartedSlide.tsx
new file mode 100644
index 0000000..a1a4b2b
--- /dev/null
+++ b/apps/tutorial/src/components/slides/GetStartedSlide.tsx
@@ -0,0 +1,128 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+const links = [
+ {
+ label: 'Browse Tools',
+ href: 'https://tpmjs.com/tool-search',
+ description: 'Explore the registry',
+ icon: '🔍',
+ gradient: 'from-cyan-500 to-blue-500',
+ },
+ {
+ label: 'Publish Your Tool',
+ href: 'https://tpmjs.com/publish',
+ description: 'Share with the community',
+ icon: '📦',
+ gradient: 'from-purple-500 to-pink-500',
+ },
+ {
+ label: 'Read the Docs',
+ href: 'https://tpmjs.com/docs',
+ description: 'Learn everything',
+ icon: '📚',
+ gradient: 'from-emerald-500 to-teal-500',
+ },
+];
+
+export function GetStartedSlide(): React.ReactElement {
+ return (
+
+ {/* Celebratory background */}
+
+
+
+
+
+
+ 🚀
+
+
+ Get Started
+ Join the future of AI tooling
+
+ {/* CTA buttons */}
+
+ {links.map((link, index) => (
+
+
+
{link.icon}
+
+
{link.label}
+
{link.description}
+
+
+
+ {/* Hover arrow */}
+
+
+
+
+ ))}
+
+
+ {/* Footer */}
+
+ Made with ❤️ for the AI community
+
+
+
+ );
+}
diff --git a/apps/tutorial/src/components/slides/HowItWorksSlide.tsx b/apps/tutorial/src/components/slides/HowItWorksSlide.tsx
new file mode 100644
index 0000000..5041aab
--- /dev/null
+++ b/apps/tutorial/src/components/slides/HowItWorksSlide.tsx
@@ -0,0 +1,90 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+const steps = [
+ { icon: '📦', label: 'npm publish', description: 'Publish your tool to npm' },
+ { icon: '🔍', label: 'Discovery', description: 'Automatic detection' },
+ { icon: '📋', label: 'Registry', description: 'Added to TPMJS' },
+ { icon: '🤖', label: 'AI Agent', description: 'Ready to use' },
+];
+
+export function HowItWorksSlide(): React.ReactElement {
+ return (
+
+ {/* Background gradient */}
+
+
+
+ How It Works
+ From npm to AI in 4 simple steps
+
+ {/* Flow diagram */}
+
+ {steps.map((step, index) => (
+
+ {/* Step card */}
+
+
+ {step.icon}
+
+ {step.label}
+ {step.description}
+
+
+ {/* Arrow */}
+ {index < steps.length - 1 && (
+
+
+
+
+ )}
+
+ ))}
+
+
+ {/* Animated highlight line */}
+
+
+
+ );
+}
diff --git a/apps/tutorial/src/components/slides/IntegrationSlide.tsx b/apps/tutorial/src/components/slides/IntegrationSlide.tsx
new file mode 100644
index 0000000..a876f59
--- /dev/null
+++ b/apps/tutorial/src/components/slides/IntegrationSlide.tsx
@@ -0,0 +1,130 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import { useEffect, useState } from 'react';
+
+const codeLines = [
+ { text: "import { tool } from '@tpmjs/sdk';", delay: 0.5 },
+ { text: '', delay: 0.8 },
+ { text: '// Get any tool from the registry', delay: 1.0 },
+ { text: "const webSearch = await tool('web-search');", delay: 1.3 },
+ { text: '', delay: 1.6 },
+ { text: '// Use it with your AI agent', delay: 1.8 },
+ { text: 'const result = await webSearch.execute({', delay: 2.1 },
+ { text: " query: 'latest AI news'", delay: 2.4 },
+ { text: '});', delay: 2.7 },
+];
+
+function TypewriterLine({ text, delay }: { text: string; delay: number }) {
+ const [displayText, setDisplayText] = useState('');
+
+ useEffect(() => {
+ if (!text) {
+ setDisplayText('');
+ return;
+ }
+
+ const timeout = setTimeout(() => {
+ let i = 0;
+ const interval = setInterval(() => {
+ if (i <= text.length) {
+ setDisplayText(text.slice(0, i));
+ i++;
+ } else {
+ clearInterval(interval);
+ }
+ }, 30);
+
+ return () => clearInterval(interval);
+ }, delay * 1000);
+
+ return () => clearTimeout(timeout);
+ }, [text, delay]);
+
+ // Syntax highlighting
+ const highlightedText = displayText
+ .replace(/(import|from|const|await)/g, '$1')
+ .replace(/('.*?')/g, '$1')
+ .replace(/(\/\/.*)/g, '$1')
+ .replace(/(\{|\}|\(|\))/g, '$1');
+
+ return (
+
+ );
+}
+
+export function IntegrationSlide(): React.ReactElement {
+ return (
+
+ {/* Background */}
+
+
+
+
+ 🔌
+
+
+ Simple Integration
+ Three lines of code. That's it.
+
+ {/* Code block */}
+
+ {/* Window chrome */}
+
+
+ {/* Code content */}
+
+ {codeLines.map((line, index) => (
+
+ ))}
+
+
+
+
+ {/* Framework badges */}
+
+ {['Vercel AI SDK', 'LangChain', 'Claude', 'OpenAI'].map((framework) => (
+
+ Works with {framework}
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/tutorial/src/components/slides/ProblemSlide.tsx b/apps/tutorial/src/components/slides/ProblemSlide.tsx
new file mode 100644
index 0000000..3998d54
--- /dev/null
+++ b/apps/tutorial/src/components/slides/ProblemSlide.tsx
@@ -0,0 +1,105 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+const floatingIcons = [
+ { icon: '🔧', x: '15%', y: '20%', delay: 0 },
+ { icon: '⚙️', x: '75%', y: '15%', delay: 0.2 },
+ { icon: '🔌', x: '85%', y: '60%', delay: 0.4 },
+ { icon: '📦', x: '10%', y: '70%', delay: 0.6 },
+ { icon: '🛠️', x: '60%', y: '80%', delay: 0.8 },
+ { icon: '🔗', x: '25%', y: '45%', delay: 1 },
+ { icon: '📡', x: '80%', y: '35%', delay: 1.2 },
+ { icon: '💾', x: '40%', y: '25%', delay: 1.4 },
+];
+
+export function ProblemSlide(): React.ReactElement {
+ return (
+
+ {/* Floating chaotic icons */}
+ {floatingIcons.map((item, index) => (
+
+ {item.icon}
+
+ ))}
+
+ {/* Red warning gradient */}
+
+
+ {/* Content */}
+
+
+ 😵
+
+
+ The Problem
+
+
+ AI tools are everywhere.
+
+ Finding and trusting them is hard.
+
+
+
+ {['Scattered', 'Inconsistent', 'Unverified', 'Fragmented'].map((word, i) => (
+
+ {word}
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/tutorial/src/components/slides/QualitySlide.tsx b/apps/tutorial/src/components/slides/QualitySlide.tsx
new file mode 100644
index 0000000..3efb9f2
--- /dev/null
+++ b/apps/tutorial/src/components/slides/QualitySlide.tsx
@@ -0,0 +1,135 @@
+'use client';
+
+import { animate, motion, useMotionValue } from 'framer-motion';
+import { useEffect, useState } from 'react';
+
+function AnimatedGauge({ value, delay = 0 }: { value: number; delay?: number }) {
+ const progress = useMotionValue(0);
+ const [displayValue, setDisplayValue] = useState(0);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ animate(progress, value, {
+ duration: 1.5,
+ ease: 'easeOut',
+ });
+ }, delay * 1000);
+
+ return () => clearTimeout(timeout);
+ }, [progress, value, delay]);
+
+ useEffect(() => {
+ const unsubscribe = progress.on('change', (v) => setDisplayValue(Math.round(v)));
+ return unsubscribe;
+ }, [progress]);
+
+ const circumference = 2 * Math.PI * 45;
+ const strokeDashoffset = circumference - (displayValue / 100) * circumference;
+
+ return (
+
+
+
+ {displayValue}%
+
+
+ );
+}
+
+const qualityFactors = [
+ { name: 'Schema Completeness', icon: '📋', score: 95, color: 'cyan' },
+ { name: 'npm Downloads', icon: '📈', score: 78, color: 'purple' },
+ { name: 'Documentation', icon: '📚', score: 88, color: 'emerald' },
+ { name: 'Type Safety', icon: '🛡️', score: 100, color: 'yellow' },
+];
+
+export function QualitySlide(): React.ReactElement {
+ return (
+
+ {/* Background */}
+
+
+
+
+ ⭐
+
+
+ Quality Scoring
+ Every tool is scored for reliability
+
+ {/* Main gauge */}
+
+
+
+
+ {/* Quality factors */}
+
+ {qualityFactors.map((factor, index) => (
+
+ {factor.icon}
+ {factor.name}
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/tutorial/src/components/slides/SolutionSlide.tsx b/apps/tutorial/src/components/slides/SolutionSlide.tsx
new file mode 100644
index 0000000..2a362e3
--- /dev/null
+++ b/apps/tutorial/src/components/slides/SolutionSlide.tsx
@@ -0,0 +1,86 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+const gridItems = Array.from({ length: 12 }, (_, i) => ({
+ icon: ['🔧', '⚙️', '📦', '🔌', '🛠️', '📡', '💾', '🔗', '🎯', '⚡', '🌐', '🔐'][i],
+ delay: i * 0.05,
+}));
+
+export function SolutionSlide(): React.ReactElement {
+ return (
+
+ {/* Green success gradient */}
+
+
+ {/* Content */}
+
+
+ ✨
+
+
+ The Solution
+
+
+ One registry.
+
+ Curated tools. Quality guaranteed.
+
+
+ {/* Organized grid of icons */}
+
+ {gridItems.map((item, index) => (
+
+ {item.icon}
+
+ ))}
+
+
+
+ {['Organized', 'Consistent', 'Verified', 'Unified'].map((word, i) => (
+
+ {word}
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/tutorial/src/components/slides/ToolDetailSlide.tsx b/apps/tutorial/src/components/slides/ToolDetailSlide.tsx
new file mode 100644
index 0000000..85a48d1
--- /dev/null
+++ b/apps/tutorial/src/components/slides/ToolDetailSlide.tsx
@@ -0,0 +1,109 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+export function ToolDetailSlide(): React.ReactElement {
+ return (
+
+ {/* Background */}
+
+
+
+ Rich Tool Data
+ Everything you need to know about each tool
+
+ {/* Mock tool card */}
+
+ {/* Header */}
+
+
+
+
+ 🔍
+
+
+
webSearch
+ @tpmjs/web-tools
+
+
+
+
+ Verified
+
+
+
+ {/* Description */}
+
+ Search the web and return structured results. Supports query filtering, result limits,
+ and domain restrictions.
+
+
+ {/* Metadata grid */}
+
+
+
+
+
+
+ {/* Schema preview */}
+
+ // Input Schema
+
+ query: string
+
+
+ limit?: number
+
+
+ domains?: string[]
+
+
+
+
+
+ );
+}
diff --git a/apps/tutorial/src/components/slides/WelcomeSlide.tsx b/apps/tutorial/src/components/slides/WelcomeSlide.tsx
new file mode 100644
index 0000000..a0ea09b
--- /dev/null
+++ b/apps/tutorial/src/components/slides/WelcomeSlide.tsx
@@ -0,0 +1,95 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+export function WelcomeSlide(): React.ReactElement {
+ return (
+
+ {/* Animated background orbs */}
+
+
+
+
+
+ {/* Logo / Title */}
+
+
+
+ TPMJS
+
+
+
+
+ {/* Tagline */}
+
+ Tool Package Manager for AI Agents
+
+
+ {/* Decorative line */}
+
+
+ {/* CTA hint */}
+
+
+
+
+ Press any key to begin
+
+
+ );
+}
diff --git a/apps/tutorial/src/hooks/useSlideshow.ts b/apps/tutorial/src/hooks/useSlideshow.ts
new file mode 100644
index 0000000..7ca121b
--- /dev/null
+++ b/apps/tutorial/src/hooks/useSlideshow.ts
@@ -0,0 +1,129 @@
+import { useCallback, useEffect, useState } from 'react';
+
+export interface UseSlideshowOptions {
+ totalSlides: number;
+ autoPlay?: boolean;
+ autoPlayInterval?: number;
+ loop?: boolean;
+}
+
+export interface UseSlideshowReturn {
+ currentSlide: number;
+ totalSlides: number;
+ next: () => void;
+ prev: () => void;
+ goTo: (index: number) => void;
+ progress: number;
+ direction: 'left' | 'right' | null;
+ isFirst: boolean;
+ isLast: boolean;
+ isAutoPlaying: boolean;
+ toggleAutoPlay: () => void;
+}
+
+export function useSlideshow({
+ totalSlides,
+ autoPlay = false,
+ autoPlayInterval = 5000,
+ loop = false,
+}: UseSlideshowOptions): UseSlideshowReturn {
+ const [currentSlide, setCurrentSlide] = useState(0);
+ const [direction, setDirection] = useState<'left' | 'right' | null>(null);
+ const [isAutoPlaying, setIsAutoPlaying] = useState(autoPlay);
+
+ const isFirst = currentSlide === 0;
+ const isLast = currentSlide === totalSlides - 1;
+ const progress = totalSlides > 1 ? currentSlide / (totalSlides - 1) : 0;
+
+ const next = useCallback(() => {
+ if (isLast && !loop) return;
+ setDirection('right');
+ setCurrentSlide((prev) => (prev + 1) % totalSlides);
+ }, [isLast, loop, totalSlides]);
+
+ const prev = useCallback(() => {
+ if (isFirst && !loop) return;
+ setDirection('left');
+ setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides);
+ }, [isFirst, loop, totalSlides]);
+
+ const goTo = useCallback(
+ (index: number) => {
+ if (index < 0 || index >= totalSlides) return;
+ setDirection(index > currentSlide ? 'right' : 'left');
+ setCurrentSlide(index);
+ },
+ [currentSlide, totalSlides]
+ );
+
+ const toggleAutoPlay = useCallback(() => {
+ setIsAutoPlaying((prev) => !prev);
+ }, []);
+
+ // Keyboard navigation
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ // Pause auto-play on any key press
+ if (isAutoPlaying) {
+ setIsAutoPlaying(false);
+ }
+
+ switch (e.key) {
+ case 'ArrowRight':
+ case ' ':
+ case 'Enter':
+ e.preventDefault();
+ next();
+ break;
+ case 'ArrowLeft':
+ e.preventDefault();
+ prev();
+ break;
+ case 'Home':
+ e.preventDefault();
+ goTo(0);
+ break;
+ case 'End':
+ e.preventDefault();
+ goTo(totalSlides - 1);
+ break;
+ case 'Escape':
+ e.preventDefault();
+ setIsAutoPlaying(false);
+ break;
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [next, prev, goTo, totalSlides, isAutoPlaying]);
+
+ // Auto-play
+ useEffect(() => {
+ if (!isAutoPlaying) return;
+
+ const timer = setInterval(() => {
+ if (isLast && !loop) {
+ setIsAutoPlaying(false);
+ return;
+ }
+ next();
+ }, autoPlayInterval);
+
+ return () => clearInterval(timer);
+ }, [isAutoPlaying, autoPlayInterval, next, isLast, loop]);
+
+ return {
+ currentSlide,
+ totalSlides,
+ next,
+ prev,
+ goTo,
+ progress,
+ direction,
+ isFirst,
+ isLast,
+ isAutoPlaying,
+ toggleAutoPlay,
+ };
+}
diff --git a/apps/tutorial/tailwind.config.ts b/apps/tutorial/tailwind.config.ts
new file mode 100644
index 0000000..a4aea36
--- /dev/null
+++ b/apps/tutorial/tailwind.config.ts
@@ -0,0 +1,11 @@
+import baseConfig from '@tpmjs/tailwind-config/base';
+import type { Config } from 'tailwindcss';
+
+export default {
+ ...baseConfig,
+ content: [
+ './src/app/**/*.{ts,tsx}',
+ './src/components/**/*.{ts,tsx}',
+ '../../packages/ui/src/**/*.ts',
+ ],
+} satisfies Config;
diff --git a/apps/tutorial/tsconfig.json b/apps/tutorial/tsconfig.json
new file mode 100644
index 0000000..cff0c6e
--- /dev/null
+++ b/apps/tutorial/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@tpmjs/tsconfig/nextjs.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"],
+ "~/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d54513a..791c130 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -134,10 +134,10 @@ importers:
version: 10.4.22(postcss@8.5.6)
eslint:
specifier: ^9.39.1
- version: 9.39.1(jiti@2.6.1)
+ version: 9.39.1(jiti@1.21.7)
eslint-config-next:
specifier: ^16.0.4
- version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
+ version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
postcss:
specifier: ^8.5.1
version: 8.5.6
@@ -157,6 +157,64 @@ importers:
specifier: ^4.18.2
version: 4.22.1
+ apps/tutorial:
+ dependencies:
+ '@tpmjs/ui':
+ specifier: workspace:*
+ version: link:../../packages/ui
+ '@tpmjs/utils':
+ specifier: workspace:*
+ version: link:../../packages/utils
+ framer-motion:
+ specifier: ^11.15.0
+ version: 11.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ next:
+ specifier: ^16.0.8
+ version: 16.0.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react:
+ specifier: ^19.0.0
+ version: 19.2.0
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.2.0(react@19.2.0)
+ devDependencies:
+ '@tpmjs/eslint-config':
+ specifier: workspace:*
+ version: link:../../packages/config/eslint
+ '@tpmjs/tailwind-config':
+ specifier: workspace:*
+ version: link:../../packages/config/tailwind
+ '@tpmjs/tsconfig':
+ specifier: workspace:*
+ version: link:../../packages/config/tsconfig
+ '@types/node':
+ specifier: ^22.10.2
+ version: 22.19.1
+ '@types/react':
+ specifier: ^19.0.2
+ version: 19.2.7
+ '@types/react-dom':
+ specifier: ^19.0.2
+ version: 19.2.3(@types/react@19.2.7)
+ autoprefixer:
+ specifier: ^10.4.20
+ version: 10.4.22(postcss@8.5.6)
+ eslint:
+ specifier: ^9.39.1
+ version: 9.39.1(jiti@2.6.1)
+ eslint-config-next:
+ specifier: ^16.0.4
+ version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.6
+ tailwindcss:
+ specifier: ^3.4.17
+ version: 3.4.18(tsx@4.21.0)
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+
apps/web:
dependencies:
'@ai-sdk/openai':
@@ -267,10 +325,10 @@ importers:
version: 17.2.3
eslint:
specifier: ^9.39.1
- version: 9.39.1(jiti@1.21.7)
+ version: 9.39.1(jiti@2.6.1)
eslint-config-next:
specifier: ^16.0.4
- version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
+ version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
postcss:
specifier: ^8.5.1
version: 8.5.6
@@ -4010,6 +4068,20 @@ packages:
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
+ framer-motion@11.18.2:
+ resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==}
+ peerDependencies:
+ '@emotion/is-prop-valid': '*'
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/is-prop-valid':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@@ -4952,6 +5024,12 @@ packages:
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
+ motion-dom@11.18.1:
+ resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==}
+
+ motion-utils@11.18.1:
+ resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
+
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@@ -10131,6 +10209,15 @@ snapshots:
fraction.js@5.3.4: {}
+ framer-motion@11.18.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ motion-dom: 11.18.1
+ motion-utils: 11.18.1
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+
fresh@0.5.2: {}
fs-extra@7.0.1:
@@ -11359,6 +11446,12 @@ snapshots:
pkg-types: 1.3.1
ufo: 1.6.1
+ motion-dom@11.18.1:
+ dependencies:
+ motion-utils: 11.18.1
+
+ motion-utils@11.18.1: {}
+
mri@1.2.0: {}
ms@2.0.0: {}