shadimage
spaceui
blockblock-interactive-fun-factssource: ok · hit

Interactive 3D Fun Facts Game

Interactive 3D stacked deck quiz game block with True/False feedback, live score and dialog summary.

npx shadcn add "https://www.spaceui.one/r/block-interactive-fun-facts.json"
free

Visual style

3dplayful

Interaction

modal

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@tabler/icons-react

Registry dependencies

https://www.spaceui.one/r/lib-utils.jsonhttps://www.spaceui.one/r/components-spaceui-badge-squircle.jsonhttps://www.spaceui.one/r/components-spaceui-button-squircle.jsonhttps://www.spaceui.one/r/primitives-card.jsonhttps://www.spaceui.one/r/primitives-dialog.jsonhttps://www.spaceui.one/r/primitives-toast.json

Files4 · source cached 10/9/2026, 8:47:41 AM

src/registry/blocks/interactive-fun-facts/interactive-fun-facts-1/index.tsx
'use client'

import { useState } from 'react'
import { cn } from '@/lib/utils'
import { toastManager } from '@/components/ui/toast'
import { Button } from '@/components/spaceui/button-squircle'
import { Badge } from '@/components/spaceui/badge-squircle'
import { questionsLocale, type Question, type GameItem } from './data'
import { GuessButton, QuestionCard } from './question-card'
import { AllFactsModal, CustomAlert } from './modals'

export function SectionHeader({
  title,
  description,
  link,
  badge,
  layoutStart,
  isPage,
}: {
  title?: string
  description?: string
  link?: string
  badge?: string
  layoutStart?: boolean
  isPage?: boolean
}) {
  return (
    <div
      className={cn(
        'max-w-2xl flex flex-col items-start justify-start gap-4 mb-12',
        !layoutStart && ' mx-auto items-center justify-center text-center',
      )}
    >
      {badge && <Badge size="md">{badge}</Badge>}

      {title && (
        <h2 className={cn(layoutStart ? 'h2 md:h3 font-normals' : 'capitalize', isPage && 'font-normal')}>{title}</h2>
      )}

      {description && (
        <p
          className={cn('text-xl md:text-2xl text-muted-foreground whitespace-pre-line', !layoutStart && 'font-medium')}
        >
          {description}
        </p>
      )}
    </div>
  )
}

export function SectionLayout({
  id,
  title,
  description,
  className,
  contentClassName,
  link,
  badge,
  children,
  isFlex,
  layoutStart,
  isPage,
}: {
  id?: string
  title?: string
  description?: string
  className?: string
  contentClassName?: string
  link?: string
  badge?: string
  children: React.ReactNode
  isFlex?: boolean
  layoutStart?: boolean
  isPage?: boolean
}) {
  return (
    <section className={cn('relative w-full py-14 md:py-26 px-2 md:px-8', className)} id={id}>
      {(title || badge) && (
        <SectionHeader
          title={title}
          description={description}
          link={link}
          badge={badge}
          layoutStart={layoutStart}
          isPage={isPage}
        />
      )}
      <div
        className={cn(
          'flex flex-col max-w-5xl mx-auto items-center justify-center justify-items-center self-center place-self-center w-full gap-6',
          !isFlex && 'md:grid grid-cols-1 md:grid-cols-2 md:max-w-[90%] place-items-center place-self-center',
          contentClassName,
        )}
      >
        {children}
      </div>
    </section>
  )
}

// Interactive Fun Facts Component
export function InteractiveFunFacts() {
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
  const [guessedFacts, setGuessedFacts] = useState<{ [key: number]: boolean }>({})
  const [showAlert, setShowAlert] = useState(false)
  const [alertData, setAlertData] = useState<{
    isCorrect: boolean
    question: Question | null
  }>({ isCorrect: false, question: null })
  const [score, setScore] = useState(0)
  const [gameStarted, setGameStarted] = useState(true)
  const [showAllFacts, setShowAllFacts] = useState(false)

  // Derive calculations based on guessedFacts
  const answeredQuestionsCount = Object.keys(guessedFacts).length
  const allQuestionsAnswered = answeredQuestionsCount === questionsLocale.length

  // Verify if there is a false in the guessed answers
  const hasGuessedFalse = Object.values(guessedFacts).includes(false)

  const handleEmojiClick = (isTrue: boolean) => {
    const currentQuestion = questionsLocale[currentQuestionIndex]
    if (!currentQuestion || guessedFacts[currentQuestion.id] !== undefined) {
      return
    }

    const isCorrect = currentQuestion.isTrue === isTrue

    if (isCorrect) {
      setScore((prev) => prev + 1)
    }

    setGuessedFacts((prev) => ({ ...prev, [currentQuestion.id]: isCorrect }))
    setAlertData({ isCorrect, question: currentQuestion })
    setShowAlert(true)
  }

  const closeAlert = () => {
    setShowAlert(false)
    setAlertData({ isCorrect: false, question: null })

    // Pass to the next question after closing the alert
    if (currentQuestionIndex < questionsLocale.length - 1) {
      setTimeout(() => {
        setCurrentQuestionIndex((prev) => prev + 1)
      }, 300)
    }
  }

  const resetGame = () => {
    setCurrentQuestionIndex(0)
    setGuessedFacts({})
    setGameStarted(true)
    setScore(0)
    toastManager.add({
      type: 'success',
      title: 'Game Reset!',
      description: 'Ready for another round of facts! 🚀',
    })
  }

  const canAnswer = gameStarted && !showAllFacts && !allQuestionsAnswered
  const badge = gameStarted || showAllFacts ? `Score: ${score}/${questionsLocale.length}` : 'Fun Facts'

  return (
    <SectionLayout
      isFlex
      badge={badge}
      title="Interactive Fun Facts"
      description="Can you spot the difference between real milestones and hilarious myths?"
    >
      {/* Main Game Zone */}
      <div className="content-center flex flex-col md:flex-row gap-4 md:gap-6 items-center justify-between w-full">
        <div className="flex flex-row gap-6 sm:gap-0 justify-center w-full sm:w-auto order-2 sm:order-1">
          <GuessButton isFalse onClick={() => handleEmojiClick(false)} canAnswer={canAnswer} />
          <div className="block sm:hidden">
            <GuessButton onClick={() => handleEmojiClick(true)} canAnswer={canAnswer} />
          </div>
        </div>

        {/* Cards Zone */}
        <div className="grid grid-cols-1 grid-rows-1 place-items-center w-full max-w-xl order-1 sm:order-2 py-8 md:py-14">
          {questionsLocale.map((question, index) => (
            <QuestionCard
              key={question.id}
              question={question}
              index={index}
              currentQuestionIndex={currentQuestionIndex}
              totalQuestions={questionsLocale.length}
            />
          ))}
        </div>

        <div className="hidden sm:block order-3">
          <GuessButton onClick={() => handleEmojiClick(true)} canAnswer={canAnswer} />
        </div>
      </div>

      {showAllFacts && (
        <AllFactsModal
          isOpen={showAllFacts}
          onClose={() => setShowAllFacts(false)}
          questions={questionsLocale}
          guessedFacts={guessedFacts}
        />
      )}

      <div className="relative">
        {/* Responsive progression indicators */}
        <div className="flex gap-2 sm:gap-3 z-10">
          {questionsLocale.map((question, index) => {
            const isGuessed = guessedFacts[question.id] !== undefined
            const isCorrect = isGuessed && guessedFacts[question.id]

            return (
              <div
                key={index}
                className={cn(
                  'size-3 sm:size-4 rounded-full border-2 flex items-center justify-center',
                  !allQuestionsAnswered && 'hover:scale-110 transition-transform duration-300 ease-in-out',
                  isGuessed
                    ? isCorrect
                      ? 'bg-green-500 border-green-500'
                      : 'bg-red-500 border-red-500'
                    : index === currentQuestionIndex
                      ? 'bg-[#2a2a2a] border-[#2a2a2a]'
                      : 'bg-muted border-border',
                )}
              >
                {isGuessed && <span className="text-white text-[6px] sm:text-[8px]">{isCorrect ? '✓' : '✗'}</span>}
              </div>
            )
          })}
        </div>
      </div>

      <div className="flex flex-col sm:flex-row gap-3 md:gap-4">
        {/* Show all facts button */}
        <Button
          onClick={() => {
            setShowAllFacts(true)
          }}
          size="lg"
          className="cursor-pointer"
        >
          {allQuestionsAnswered
            ? hasGuessedFalse
              ? 'See Answers & Context 📜'
              : 'View Perfect Summary 🎉'
            : showAllFacts
              ? 'Retry 🚀'
              : 'Explore All Facts 📜'}
        </Button>

        {allQuestionsAnswered && (
          <div className="text-center">
            <Button onClick={resetGame} variant="secondary" size="lg" className="cursor-pointer">
              Play Again 🚀
            </Button>
          </div>
        )}
      </div>

      {/* Personalized alert */}
      <CustomAlert
        isVisible={showAlert}
        isCorrect={alertData.isCorrect}
        question={alertData.question}
        onClose={closeAlert}
      />
    </SectionLayout>
  )
}

export default function Particle() {
  return <InteractiveFunFacts />
}
src/registry/blocks/interactive-fun-facts/interactive-fun-facts-1/data.ts
export interface GameItem {
  id: number
  emoji: string
  title: string
  description: string
  subtitle: string
  isTrue: boolean
  funFact?: string
  funnyTruthMessage?: string
  funnyLieMessage?: string
}

export type Question = GameItem

export const questionsLocale: Question[] = [
  {
    id: 1,
    emoji: '🐛',
    title: 'The first computer bug was an actual insect',
    description: 'A physical moth was trapped in a relay of the Harvard Mark II in 1947.',
    subtitle: 'Literal debugging.',
    isTrue: true,
    funFact: 'Grace Hopper taped the moth into the logbook.',
    funnyTruthMessage: 'A real moth taped in the logbook!',
    funnyLieMessage: 'It was a real moth trapped inside the machine.',
  },
  {
    id: 2,
    emoji: '☕',
    title: 'JavaScript was created to be Java for the web',
    description: 'Netscape rebranded Mocha to JavaScript purely to ride Java’s hype in 1995.',
    subtitle: 'Ham is to hamster as Java is to JavaScript.',
    isTrue: false,
    funFact: 'Brendan Eich built it in 10 days. They share zero code.',
    funnyTruthMessage: 'Zero shared code—pure 90s marketing hype!',
    funnyLieMessage: 'They share no architecture at all.',
  },
  {
    id: 3,
    emoji: '🍓',
    title: 'AI models can easily count letters in words',
    description: 'LLMs often fail simple letter counts like the "r"s in "strawberry".',
    subtitle: 'How many "r"s in strawberry?',
    isTrue: false,
    funFact: 'Tokenizers process word chunks, not individual letters.',
    funnyTruthMessage: 'Tokenizers see chunks, not letters!',
    funnyLieMessage: 'Tokenizers process chunks, so letter counting trips them up.',
  },
  {
    id: 4,
    emoji: '🎮',
    title: 'Nvidia GPUs were originally made only for video games',
    description: 'Founded in 1993, Nvidia built chips strictly for 3D PC gaming graphics.',
    subtitle: 'From Doom to Deep Learning.',
    isTrue: true,
    funFact: 'Jensen Huang bet on CUDA in 2006, years before the AI boom.',
    funnyTruthMessage: 'Built for gamers long before modern AI.',
    funnyLieMessage: '100% true—designed strictly for PC gaming.',
  },
  {
    id: 5,
    emoji: '💾',
    title: 'A 3.5-inch floppy disk held less than 2 MB',
    description: 'The universal physical save icon held a grand total of only 1.44 MB.',
    subtitle: 'Less than one smartphone photo.',
    isTrue: true,
    funFact: 'A modern smartphone photo needs at least 3 floppy disks.',
    funnyTruthMessage: '1.44 MB max—not even one modern song!',
    funnyLieMessage: '1.44 MB was all you had for entire games.',
  },
  {
    id: 6,
    emoji: '🔄',
    title: 'Training AI on AI-generated data makes it smarter',
    description: 'Feeding models synthetic text from other AIs improves accuracy.',
    subtitle: 'The synthetic data paradox.',
    isTrue: false,
    funFact: 'It causes "Model Collapse", degrading outputs into gibberish.',
    funnyTruthMessage: 'It causes Model Collapse—AI needs fresh human data.',
    funnyLieMessage: 'Recursive AI data causes Model Collapse.',
  },
  {
    id: 7,
    emoji: '🕹️',
    title: 'Nintendo was founded in 1889 as a card company',
    description: 'Nintendo began in Kyoto over 135 years ago making Hanafuda playing cards.',
    subtitle: 'Over 135 years of history.',
    isTrue: true,
    funFact: 'They made playing cards for 80 years before their first arcade game.',
    funnyTruthMessage: 'Started in 1889 with handmade flower cards!',
    funnyLieMessage: 'Founded in 1889—long before video games existed.',
  },
  {
    id: 8,
    emoji: '🧠',
    title: 'The term "Artificial Intelligence" was coined in 1956',
    description: 'John McCarthy proposed the term at the historic Dartmouth conference.',
    subtitle: 'Decades before the personal computer.',
    isTrue: true,
    funFact: 'Researchers gathered in 1956 to explore machine thinking.',
    funnyTruthMessage: 'Coined at Dartmouth in 1956!',
    funnyLieMessage: 'AI research officially started in 1956.',
  },
  {
    id: 9,
    emoji: '🌐',
    title: 'The first .com domain was registered before the Web existed',
    description: 'Symbolics.com was registered in 1985, four years before the World Wide Web.',
    subtitle: 'Pre-web internet history.',
    isTrue: true,
    funFact: 'Tim Berners-Lee invented the web in 1989, years after symbolics.com.',
    funnyTruthMessage: 'Registered in 1985—years before the World Wide Web!',
    funnyLieMessage: 'Symbolics.com was registered 4 years before the web.',
  },
  {
    id: 10,
    emoji: '📄',
    title: 'The Transformer paper was rejected twice by NeurIPS',
    description: 'Google’s 2017 "Attention Is All You Need" paper was initially turned down.',
    subtitle: 'The foundation of ChatGPT.',
    isTrue: false,
    funFact: 'NeurIPS accepted it on its very first submission in 2017.',
    funnyTruthMessage: 'Accepted on the first try with zero rejections!',
    funnyLieMessage: 'Accepted on the first try in 2017.',
  },
]
src/registry/blocks/interactive-fun-facts/interactive-fun-facts-1/question-card.tsx
'use client'

import { cn } from '@/lib/utils'
import { Card, CardPanel } from '@/components/ui/card'
import { Badge } from '@/components/spaceui/badge-squircle'
import type { Question } from './data'

export function GuessButton({
  isFalse,
  onClick,
  canAnswer,
}: {
  isFalse?: boolean
  onClick: () => void
  canAnswer: boolean
}) {
  return (
    <div
      className={cn(
        'relative bg-background border-4 border-border content-stretch flex items-center justify-start p-4 md:p-6 rounded-full shrink-0 cursor-pointer transition-all duration-400',
        canAnswer
          ? isFalse
            ? 'hover:bg-red-100 hover:border-red-500 dark:hover:border-red-400'
            : 'hover:bg-green-100 hover:border-green-500 dark:hover:border-green-400'
          : 'pointer-events-none cursor-default',
      )}
      onClick={onClick}
    >
      <div className="relative shrink-0 size-11 md:size-20">
        <span
          className={cn(
            'size-full flex items-center justify-center text-5xl md:text-7xl object-cover pointer-events-none',
            !canAnswer && 'opacity-50',
          )}
        >
          {isFalse ? '🤥' : '😀'}
        </span>
        {canAnswer && (
          <span
            className={cn(
              'absolute -bottom-2 left-1/2 -translate-x-1/2 text-xs font-bold px-2 py-0.5 rounded-full text-white whitespace-nowrap shadow-xs',
              isFalse ? 'bg-red-500' : 'bg-green-500',
            )}
          >
            {isFalse ? 'Lie' : 'Truth'}
          </span>
        )}
      </div>
    </div>
  )
}

export function QuestionCard({
  question,
  index,
  currentQuestionIndex,
  totalQuestions,
}: {
  question: Question
  index: number
  currentQuestionIndex: number
  totalQuestions: number
}) {
  const diff = index - currentQuestionIndex

  let scale = 1
  let translateY = 0
  let rotate = 0
  let opacity = 0
  let zIndex = 0
  let pointerEvents: 'auto' | 'none' = 'none'

  if (diff === 0) {
    scale = 1
    translateY = 0
    rotate = 0
    opacity = 1
    zIndex = 30
    pointerEvents = 'auto'
  } else if (diff > 0 && diff <= 3) {
    scale = 1 - diff * 0.04
    translateY = -diff * 12
    rotate = diff === 1 ? -1.5 : diff === 2 ? 1.5 : -0.75
    opacity = 1 - diff * 0.22
    zIndex = 30 - diff
    pointerEvents = 'none'
  } else if (diff === -1) {
    scale = 0.98
    translateY = -36
    rotate = -2.5
    opacity = 0
    zIndex = 35
    pointerEvents = 'none'
  } else {
    scale = diff > 0 ? 0.88 : 0.95
    translateY = diff > 0 ? -36 : -48
    rotate = 0
    opacity = 0
    zIndex = 0
    pointerEvents = 'none'
  }

  return (
    <Card
      className={cn(
        'col-start-1 row-start-1 w-full max-w-xl h-full',
        'bg-muted rounded-3xl md:rounded-5xl overflow-hidden',
        'will-change-transform select-none',
        'transition-[transform,opacity] duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]',
      )}
      style={{
        zIndex,
        pointerEvents,
        transformOrigin: 'center center',
        transform: `translate3d(0, ${translateY}px, 0) scale(${scale}) rotate(${rotate}deg)`,
        opacity,
      }}
    >
      <CardPanel
        className={cn(
          'flex relative flex-col min-h-60 h-full items-center justify-center p-4 rounded-xl md:rounded-3xl bg-background overflow-hidden',
        )}
      >
        <div className="flex flex-col items-start gap-4 w-full max-w-[90%] py-12 mx-auto">
          <Badge
            variant="secondary"
            squircle={false}
            className="aspect-square p-4 rounded-full size-auto! [&>svg]:size-12! [&>img]:size-12!"
          >
            <span className="text-4xl">{question.emoji}</span>
          </Badge>
          <h3 className="text-3xl tracking-wide leading-[120%]">{question.title}</h3>

          <p className="text-muted-foreground text-xl leading-[140%]">{question.description}</p>
          <p className="text-zinc-400 text-lg leading-[120%]">{question.subtitle}</p>
        </div>
      </CardPanel>
    </Card>
  )
}
src/registry/blocks/interactive-fun-facts/interactive-fun-facts-1/modals.tsx
'use client'

import { useEffect } from 'react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/spaceui/button-squircle'
import { Badge } from '@/components/spaceui/badge-squircle'
import {
  Dialog,
  DialogPopup,
  DialogTitle,
  DialogDescription,
  DialogHeader,
  DialogFooter,
  DialogPanel,
} from '@/components/ui/dialog'
import type { Question } from './data'
import { IconX } from '@tabler/icons-react'

export function CustomAlert({
  isVisible,
  isCorrect,
  question,
  onClose,
}: {
  isVisible: boolean
  isCorrect: boolean
  question: Question | null
  onClose: () => void
}) {
  useEffect(() => {
    if (isVisible) {
      const timer = setTimeout(() => {
        onClose()
      }, 3500)
      return () => clearTimeout(timer)
    }
  }, [isVisible, onClose])

  return (
    <Dialog open={isVisible} onOpenChange={(open) => !open && onClose()}>
      <DialogPopup className="max-w-sm px-4 pt-2 flex flex-col" showCloseButton={false}>
        <DialogPanel className="p-4 flex flex-col gap-4 items-center text-center">
          <Badge
            variant="secondary"
            square
            squircle={false}
            className="bg-background p-0! text-[2.625rem]! rounded-2xl size-auto!"
          >
            {isCorrect ? '🎉' : '😅'}
          </Badge>

          <div className="flex flex-col gap-1.5 items-center">
            <h4
              className={cn('text-2xl font-bold tracking-tight', {
                'text-green-600 dark:text-green-400': isCorrect,
                'text-red-600 dark:text-red-400': !isCorrect,
              })}
            >
              {isCorrect ? 'Nice!' : 'Not quite!'}
            </h4>
            <DialogDescription className="text-base text-muted-foreground">
              {isCorrect ? question?.funnyTruthMessage : question?.funnyLieMessage}
            </DialogDescription>
          </div>

          {question?.funFact && (
            <div className="w-full rounded-2xl border-2 border-muted bg-card p-4 text-center">
              <Badge variant={isCorrect ? 'success' : 'warning'} size="sm" className="mb-2">
                Behind the fact 🥸
              </Badge>
              <p className="text-sm text-foreground/90 leading-relaxed">{question.funFact}</p>
            </div>
          )}
        </DialogPanel>

        <DialogFooter className="mt-2 border-t border-muted" variant="bare">
          <Button onClick={onClose} size="lg" className="w-full cursor-pointer">
            {isCorrect ? 'Continue 😍' : 'Retry 😩'}
          </Button>
        </DialogFooter>
      </DialogPopup>
    </Dialog>
  )
}

export function AllFactsModal({
  isOpen,
  onClose,
  guessedFacts,
  questions,
}: {
  isOpen: boolean
  onClose: () => void
  guessedFacts: { [key: number]: boolean }
  questions: Question[]
}) {
  const totalGuessed = Object.keys(guessedFacts).length
  const totalQuestions = questions.length
  const correctGuesses = questions.filter((fact) => guessedFacts[fact.id] !== undefined && guessedFacts[fact.id]).length

  return (
    <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
      <DialogPopup className="max-w-2xl max-h-[85vh] px-4 pt-2 flex flex-col" showCloseButton={false}>
        <DialogHeader className="flex flex-row p-3.5! items-center justify-between border-b border-muted">
          <DialogTitle className="text-base font-normal md:font-medium md:text-xl text-muted-foreground">
            Game Summary & All Facts
          </DialogTitle>
          <Button onClick={onClose} size="icon-sm" className="rounded-lg">
            <IconX className="size-5" />
          </Button>
        </DialogHeader>

        <DialogPanel className="flex flex-col gap-4 px-2 py-4 flex-1 min-h-0">
          {/* Score Section */}
          {totalGuessed > 0 && (
            <div className="rounded-2xl border-2 border-muted bg-card p-4">
              <h5 className="font-bold mb-2">
                🎯 Score: {correctGuesses} / {totalQuestions}
              </h5>
              <p className="text-sm text-muted-foreground">
                {correctGuesses === totalQuestions
                  ? 'Perfect score! You know everything.'
                  : correctGuesses > totalQuestions / 2
                    ? 'Great run! Most facts guessed correctly.'
                    : 'Good attempt! Play again to improve your score.'}
              </p>
            </div>
          )}

          {/* Facts Grid */}
          {questions.map((question) => {
            const userGuessed = guessedFacts[question.id] !== undefined
            const userGuessedCorrect = userGuessed && guessedFacts[question.id]

            return (
              <div key={question.id} className="rounded-2xl border-2 border-muted bg-card p-4">
                <div className="flex items-start gap-3 sm:gap-4">
                  <Badge variant="secondary" square squircle={false} className="text-2xl! p-3! rounded-xl size-auto!">
                    {question.emoji}
                  </Badge>

                  <div className="flex flex-col flex-1 min-w-0">
                    <h5 className="font-bold mb-1">{question.title}</h5>
                    <p className="text-base text-muted-foreground mb-3">{question.description}</p>

                    {/* Badges */}
                    <div className="flex gap-2 flex-wrap">
                      <Badge variant={question.isTrue ? 'success' : 'error'} size="sm">
                        {question.isTrue ? 'Real Truth' : 'Lie'}
                      </Badge>

                      {userGuessed && (
                        <Badge variant={userGuessedCorrect ? 'info' : 'warning'} size="sm">
                          {userGuessedCorrect ? 'Guessed Correctly' : 'Incorrect Guess'}
                        </Badge>
                      )}
                    </div>
                  </div>
                </div>
              </div>
            )
          })}
        </DialogPanel>

        <DialogFooter className="mt-4 border-t border-muted" variant="bare">
          <Button onClick={onClose} size="lg" className="w-full cursor-pointer">
            Understood 😊
          </Button>
        </DialogFooter>
      </DialogPopup>
    </Dialog>
  )
}

More from spaceui

Browse all components in this library →