{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "interactive-book", "type": "registry:ui", "dependencies": [ "framer-motion", "lucide-react" ], "files": [ { "path": "components/ui/interactive-book.tsx", "content": "\"use client\";\r\n\r\nimport React, { useState, useEffect } from 'react';\r\nimport { motion, AnimatePresence } from 'framer-motion';\r\nimport { cn } from '@/lib/utils';\r\nimport { ChevronLeft, ChevronRight, RefreshCcw, X, BookOpen } from 'lucide-react';\r\n\r\nexport interface BookPage {\r\n title?: string;\r\n content: React.ReactNode;\r\n backContent?: React.ReactNode;\r\n pageNumber: number;\r\n}\r\n\r\nexport interface InteractiveBookProps {\r\n coverImage: string;\r\n bookTitle?: string;\r\n bookAuthor?: string;\r\n pages: BookPage[];\r\n className?: string;\r\n width?: number | string;\r\n height?: number | string;\r\n}\r\n\r\nexport default function InteractiveBook({\r\n coverImage,\r\n bookTitle = \"Book Title\",\r\n bookAuthor = \"Author Name\",\r\n pages,\r\n className,\r\n width = 350,\r\n height = 500,\r\n}: InteractiveBookProps) {\r\n const [isOpen, setIsOpen] = useState(false);\r\n const [currentPageIndex, setCurrentPageIndex] = useState(-1);\r\n const [isHovering, setIsHovering] = useState(false);\r\n\r\n // Calculate dynamic width/height values for animations\r\n const widthNum = typeof width === 'number' ? width : 350;\r\n\r\n // Sync container shift with cover open\r\n const BOOK_OPEN_DURATION = 1.5;\r\n const EASING: [number, number, number, number] = [0.25, 0, 0, 1]; // milder smoothing\r\n\r\n const handleOpenBook = () => setIsOpen(true);\r\n\r\n const handleCloseBook = (e?: React.MouseEvent) => {\r\n e?.stopPropagation();\r\n setIsOpen(false);\r\n setCurrentPageIndex(-1);\r\n };\r\n\r\n const nextPage = (e?: React.MouseEvent) => {\r\n e?.stopPropagation();\r\n if (currentPageIndex < pages.length - 1) {\r\n setCurrentPageIndex((prev) => prev + 1);\r\n }\r\n };\r\n\r\n const prevPage = (e?: React.MouseEvent) => {\r\n e?.stopPropagation();\r\n if (currentPageIndex >= 0) {\r\n setCurrentPageIndex((prev) => prev - 1);\r\n }\r\n };\r\n\r\n const restartBook = (e?: React.MouseEvent) => {\r\n e?.stopPropagation();\r\n setCurrentPageIndex(-1);\r\n };\r\n\r\n const handleSliderChange = (e: React.ChangeEvent) => {\r\n setCurrentPageIndex(parseInt(e.target.value, 10));\r\n };\r\n\r\n // Keyboard navigation\r\n useEffect(() => {\r\n if (!isOpen) return;\r\n const handleKeyDown = (e: KeyboardEvent) => {\r\n if (e.key === 'ArrowRight') nextPage();\r\n if (e.key === 'ArrowLeft') prevPage();\r\n if (e.key === 'Escape') handleCloseBook();\r\n };\r\n window.addEventListener('keydown', handleKeyDown);\r\n return () => window.removeEventListener('keydown', handleKeyDown);\r\n }, [isOpen, currentPageIndex]);\r\n\r\n return (\r\n \r\n \r\n\r\n {/* Front Cover */}\r\n !isOpen && setIsHovering(true)}\r\n onHoverEnd={() => setIsHovering(false)}\r\n >\r\n {/* Front Face */}\r\n \r\n {/* Image Background */}\r\n \r\n
\r\n\r\n
\r\n

{bookTitle}

\r\n

{bookAuthor}

\r\n
\r\n\r\n {/* Spine Highlight */}\r\n
\r\n
\r\n
\r\n\r\n {/* Back Face (Inner Cover) */}\r\n {\r\n e.stopPropagation();\r\n prevPage();\r\n }}\r\n >\r\n
\r\n

{bookTitle}

\r\n
\r\n

Interactive Edition

\r\n
\r\n
\r\n \r\n\r\n {/* Pages Stack */}\r\n
\r\n {pages.map((page, index) => {\r\n const isFlipped = index <= currentPageIndex;\r\n // Stagger delays slightly for a realistic \"whip\" effect if user clicks fast, \r\n // but mostly we want instant feedback with smooth transition.\r\n\r\n return (\r\n \r\n {/* Front Face (Right Side) */}\r\n {\r\n e.stopPropagation();\r\n nextPage();\r\n }}\r\n >\r\n
\r\n
\r\n {page.pageNumber * 2 - 1}\r\n
\r\n
\r\n {page.title && (\r\n

\r\n {page.title}\r\n

\r\n )}\r\n {page.content}\r\n
\r\n
\r\n
\r\n
\r\n\r\n {/* Back Face (Left Side) */}\r\n {\r\n e.stopPropagation();\r\n prevPage();\r\n }}\r\n >\r\n
\r\n\r\n
\r\n
\r\n {page.pageNumber * 2}\r\n
\r\n
\r\n {page.backContent ? (\r\n
\r\n {page.backContent}\r\n
\r\n ) : (\r\n
\r\n \r\n {page.pageNumber * 2}\r\n \r\n
\r\n )}\r\n
\r\n
\r\n
\r\n \r\n );\r\n })}\r\n\r\n {/* Back Cover (Static) */}\r\n \r\n
\r\n

The End

\r\n \r\n Read Again\r\n \r\n
\r\n
\r\n
\r\n\r\n {/* Controls Bar Removed */}\r\n\r\n \r\n\r\n {/* Side Navigation Arrows */}\r\n \r\n {isOpen && (\r\n <>\r\n {/* Close Button */}\r\n \r\n \r\n \r\n \r\n )}\r\n \r\n\r\n {/* Hint */}\r\n {!isOpen && (\r\n \r\n Click to Open\r\n \r\n )}\r\n
\r\n );\r\n}\r\n", "type": "registry:ui", "target": "components/ui/interactive-book.tsx" } ] }