{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scratch-card-popup",
  "title": "Scratch Card Popup",
  "description": "A gamified scratch-to-win popup, drag to scratch a gold-foil canvas off a 3x3 icon grid, match three to win a reward tier with a confetti burst, copy the code, and track attempts left. Inline or corner-widget popup with manual, delay or scroll triggers.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/new-york/scratch-card-popup/scratch-card-popup.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nfunction cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\ninterface Prize {\n  title: string;\n  description: string;\n  code: string;\n}\n\ninterface ScratchTheme {\n  name: string;\n  title: string;\n  subtitle: string;\n  triggerText: string;\n  primary: string;\n  accent: string;\n  prizes: Prize[];\n}\n\nconst THEMES: Record<string, ScratchTheme> = {\n  ecommerce: {\n    name: \"E-Commerce\",\n    title: \"Unlock Your Reward\",\n    subtitle: \"Scratch the foil and match three identical items\",\n    triggerText: \"Scratch & Win\",\n    primary: \"linear-gradient(160deg, #dbeafe 0%, #bfdbfe 100%)\",\n    accent: \"#0f172a\",\n    prizes: [\n      { title: \"Congratulations\", description: \"You unlocked 50% off\", code: \"GOLD50\" },\n      { title: \"Nice!\", description: \"Free shipping unlocked\", code: \"SHIPFREE\" },\n      { title: \"Lucky you\", description: \"Mystery gift added\", code: \"GIFT25\" },\n    ],\n  },\n  campaign: {\n    name: \"Campaign / Sale\",\n    title: \"Scratch Before It's Gone\",\n    subtitle: \"Limited-time drop, scratch now\",\n    triggerText: \"Scratch for a Deal\",\n    primary: \"linear-gradient(160deg, #fff1f0 0%, #fecdd3 100%)\",\n    accent: \"#dc2626\",\n    prizes: [\n      { title: \"You got it!\", description: \"20% off your order\", code: \"SALE20\" },\n      { title: \"Boom!\", description: \"30% off your order\", code: \"SALE30\" },\n      { title: \"Jackpot\", description: \"40% off your order\", code: \"SALE40\" },\n    ],\n  },\n  restaurant: {\n    name: \"Restaurant / Cafe\",\n    title: \"Today's Lucky Scratch\",\n    subtitle: \"Scratch, reveal, treat yourself\",\n    triggerText: \"Scratch for a Treat\",\n    primary: \"linear-gradient(160deg, #fff7ed 0%, #fde8cf 100%)\",\n    accent: \"#b45309\",\n    prizes: [\n      { title: \"Cheers!\", description: \"Free coffee on us\", code: \"CAFE-COFFEE\" },\n      { title: \"Sweet!\", description: \"Free dessert with any meal\", code: \"CAFE-DESSERT\" },\n      { title: \"Treat unlocked\", description: \"15% off your bill\", code: \"CAFE15\" },\n    ],\n  },\n  saas: {\n    name: \"SaaS / Freemium\",\n    title: \"Scratch for a Plan Discount\",\n    subtitle: \"One scratch per new account\",\n    triggerText: \"Scratch & Save\",\n    primary: \"linear-gradient(160deg, #eef2ff 0%, #e0e7ff 100%)\",\n    accent: \"#4f46e5\",\n    prizes: [\n      { title: \"Nice start\", description: \"10% off your first plan\", code: \"SAAS10\" },\n      { title: \"Great pick\", description: \"1 month free\", code: \"SAAS-MONTH\" },\n      { title: \"Top tier\", description: \"30% off your first plan\", code: \"SAAS30\" },\n    ],\n  },\n  event: {\n    name: \"Event / Tickets\",\n    title: \"Scratch for an Early Bird Deal\",\n    subtitle: \"Before you check out\",\n    triggerText: \"Scratch for a Bonus\",\n    primary: \"linear-gradient(160deg, #ecfeff 0%, #cffafe 100%)\",\n    accent: \"#0891b2\",\n    prizes: [\n      { title: \"Nice!\", description: \"5% off your ticket\", code: \"EVT5\" },\n      { title: \"Sweet\", description: \"Free drink ticket\", code: \"EVT-DRINK\" },\n      { title: \"VIP\", description: \"VIP seating upgrade\", code: \"EVT-VIP\" },\n    ],\n  },\n};\n\nconst svgDataUri = (svg: string) => `data:image/svg+xml,${encodeURIComponent(svg)}`;\nconst ICON_CROWN = svgDataUri(`<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"#D4AF37\" d=\"M2 8l4 3 6-7 6 7 4-3-2 10H4L2 8zM4 20h16v2H4z\"/></svg>`);\nconst ICON_DIAMOND = svgDataUri(`<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"#60A5FA\" d=\"M6 3h12l4 6-10 12L2 9z\"/></svg>`);\nconst ICON_GIFT = svgDataUri(`<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"9\" width=\"18\" height=\"12\" fill=\"#34D399\"/><rect x=\"3\" y=\"6\" width=\"18\" height=\"4\" fill=\"#10B981\"/><rect x=\"10.5\" y=\"6\" width=\"3\" height=\"15\" fill=\"#ffffff\"/></svg>`);\nconst ICON_STAR = svgDataUri(`<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"#F472B6\" d=\"M12 2l2.9 6.26L22 9.27l-5 4.87 1.18 6.86L12 17.77l-6.18 3.23L7 14.14 2 9.27l7.1-1.01z\"/></svg>`);\nconst ICON_HEXAGON = svgDataUri(`<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"#A78BFA\" stroke-width=\"2\" d=\"M12 2l8 4.5v11L12 22l-8-4.5v-11z\"/></svg>`);\nconst ICON_COIN = svgDataUri(`<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"#FBBF24\"/><circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"#D97706\" stroke-width=\"1.5\"/><text x=\"12\" y=\"16\" font-size=\"10\" text-anchor=\"middle\" fill=\"#92400E\" font-family=\"Arial\">$</text></svg>`);\nconst ICON_HEART = svgDataUri(`<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"#F87171\" d=\"M12 21s-8-5.5-8-11a5 5 0 0 1 9-3 5 5 0 0 1 9 3c0 5.5-8 11-8 11z\"/></svg>`);\n\nconst DEFAULT_IMAGES = [ICON_CROWN, ICON_CROWN, ICON_CROWN, ICON_DIAMOND, ICON_GIFT, ICON_STAR, ICON_HEXAGON, ICON_COIN, ICON_HEART];\n\nfunction shuffle<T>(arr: T[]): T[] {\n  const copy = [...arr];\n  for (let i = copy.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1));\n    [copy[i], copy[j]] = [copy[j], copy[i]];\n  }\n  return copy;\n}\n\nfunction GiftIcon() {\n  return (\n    <svg width=\"26\" height=\"26\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n      <defs>\n        <linearGradient id=\"scratch-gift-ribbon\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n          <stop offset=\"0%\" stopColor=\"#fde68a\" />\n          <stop offset=\"100%\" stopColor=\"#f59e0b\" />\n        </linearGradient>\n      </defs>\n      <rect x=\"3.5\" y=\"10\" width=\"17\" height=\"9.5\" rx=\"1.5\" fill=\"#ffffff\" />\n      <rect x=\"2.5\" y=\"7\" width=\"19\" height=\"4\" rx=\"1\" fill=\"#ffffff\" />\n      <rect x=\"10.4\" y=\"7\" width=\"3.2\" height=\"12.5\" fill=\"url(#scratch-gift-ribbon)\" />\n      <path d=\"M12 7c-1.6 0-3-1-3.6-2.1C7.6 3.2 8.5 2 9.8 2c1.3 0 2.2 1.4 2.2 3 0-1.6.9-3 2.2-3 1.3 0 2.2 1.2 1.4 2.9C15 6 13.6 7 12 7z\" fill=\"url(#scratch-gift-ribbon)\" />\n    </svg>\n  );\n}\n\nfunction ScratchConfetti({ colors, onDone }: { colors: string[]; onDone: () => void }) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const onDoneRef = React.useRef(onDone);\n  onDoneRef.current = onDone;\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const parent = canvas?.parentElement;\n    if (!canvas || !parent) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    const w = parent.clientWidth || 300;\n    const h = parent.clientHeight || 300;\n    canvas.width = w * dpr;\n    canvas.height = h * dpr;\n    canvas.style.width = w + \"px\";\n    canvas.style.height = h + \"px\";\n    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n    interface P {\n      x: number;\n      y: number;\n      vx: number;\n      vy: number;\n      rot: number;\n      vRot: number;\n      size: number;\n      color: string;\n      alpha: number;\n    }\n\n    const count = w < 420 ? 60 : 100;\n    const particles: P[] = Array.from({ length: count }, () => {\n      const angle = Math.random() * Math.PI * 2;\n      const speed = 3 + Math.random() * 7;\n      return {\n        x: w / 2,\n        y: h * 0.32,\n        vx: Math.cos(angle) * speed,\n        vy: Math.sin(angle) * speed - 2,\n        rot: Math.random() * Math.PI * 2,\n        vRot: (Math.random() - 0.5) * 0.3,\n        size: 5 + Math.random() * 7,\n        color: colors[Math.floor(Math.random() * colors.length)],\n        alpha: 1,\n      };\n    });\n\n    const duration = 2200;\n    const start = performance.now();\n    let rafId: number;\n\n    const animate = (t: number) => {\n      const elapsed = t - start;\n      const cw = parent.clientWidth || w;\n      const ch = parent.clientHeight || h;\n      ctx.clearRect(0, 0, cw, ch);\n\n      let anyAlive = false;\n      for (const p of particles) {\n        p.vy += 0.14;\n        p.x += p.vx;\n        p.y += p.vy;\n        p.rot += p.vRot;\n        if (elapsed > duration * 0.55) p.alpha -= 0.025;\n        if (p.alpha > 0.01 && p.y < ch + 40) {\n          anyAlive = true;\n          ctx.save();\n          ctx.globalAlpha = Math.max(0, p.alpha);\n          ctx.fillStyle = p.color;\n          ctx.translate(p.x, p.y);\n          ctx.rotate(p.rot);\n          ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);\n          ctx.restore();\n        }\n      }\n\n      if (elapsed < duration && anyAlive) rafId = requestAnimationFrame(animate);\n      else onDoneRef.current();\n    };\n\n    rafId = requestAnimationFrame(animate);\n    return () => cancelAnimationFrame(rafId);\n  }, [colors]);\n\n  return <canvas ref={canvasRef} className=\"pointer-events-none absolute inset-0\" />;\n}\n\nconst BRUSH_SIZE = 60;\n\nfunction drawFoil(canvas: HTMLCanvasElement, size: number) {\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) return;\n  canvas.width = size;\n  canvas.height = size;\n\n  const gradient = ctx.createLinearGradient(0, 0, size, size);\n  gradient.addColorStop(0, \"#6b4409\");\n  gradient.addColorStop(0.18, \"#a9761a\");\n  gradient.addColorStop(0.4, \"#e7bb52\");\n  gradient.addColorStop(0.5, \"#f2cd80\");\n  gradient.addColorStop(0.6, \"#e2af42\");\n  gradient.addColorStop(0.82, \"#9c6c14\");\n  gradient.addColorStop(1, \"#5e3b07\");\n  ctx.fillStyle = gradient;\n  ctx.fillRect(0, 0, size, size);\n\n  ctx.save();\n  ctx.globalCompositeOperation = \"overlay\";\n  for (let i = 0; i < 260; i++) {\n    const y = Math.random() * size;\n    const len = size * (0.3 + Math.random() * 0.7);\n    const x = Math.random() * size - len / 2;\n    ctx.strokeStyle = `rgba(255,255,255,${Math.random() * 0.1})`;\n    ctx.lineWidth = Math.random() * 1.1;\n    ctx.beginPath();\n    ctx.moveTo(x, y);\n    ctx.lineTo(x + len, y + (Math.random() - 0.5) * 6);\n    ctx.stroke();\n  }\n  ctx.restore();\n\n  const vignette = ctx.createRadialGradient(size / 2, size / 2, size * 0.32, size / 2, size / 2, size * 0.72);\n  vignette.addColorStop(0, \"rgba(0,0,0,0)\");\n  vignette.addColorStop(1, \"rgba(50,30,4,0.28)\");\n  ctx.fillStyle = vignette;\n  ctx.fillRect(0, 0, size, size);\n\n  const drawSpacedText = (text: string, cx: number, cy: number, font: string, trackingPx: number) => {\n    ctx.font = font;\n    const chars = text.split(\"\");\n    const widths = chars.map((c) => ctx.measureText(c).width);\n    const totalWidth = widths.reduce((a, b) => a + b, 0) + trackingPx * (chars.length - 1);\n    let x = cx - totalWidth / 2;\n    ctx.textAlign = \"left\";\n    chars.forEach((c, i) => {\n      ctx.fillText(c, x, cy);\n      x += widths[i] + trackingPx;\n    });\n  };\n\n  const scratchFont = `800 15px Arial, sans-serif`;\n  const andFont = `italic 400 20px Georgia, serif`;\n  const centerY = size / 2;\n  const lineGap = 7;\n  const lines = [\n    { text: \"SCRATCH\", y: centerY - 20 - lineGap, font: scratchFont, tracking: 1.5 },\n    { text: \"and\", y: centerY, font: andFont, tracking: 0 },\n    { text: \"WIN!\", y: centerY + 20 + lineGap, font: scratchFont, tracking: 1.5 },\n  ];\n  ctx.textBaseline = \"middle\";\n  lines.forEach(({ text, y, font, tracking }) => {\n    ctx.fillStyle = \"rgba(40,25,2,0.35)\";\n    drawSpacedText(text, size / 2 + 1, y + 1, font, tracking);\n  });\n  lines.forEach(({ text, y, font, tracking }) => {\n    ctx.fillStyle = \"rgba(255,250,230,0.92)\";\n    drawSpacedText(text, size / 2, y, font, tracking);\n  });\n}\n\nexport interface ScratchCardPopupProps extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  theme?: \"ecommerce\" | \"campaign\" | \"restaurant\" | \"saas\" | \"event\";\n  asPopup?: boolean;\n  trigger?: \"manual\" | \"delay\" | \"scroll\";\n  delaySeconds?: number;\n  widgetPosition?: \"bottom-right\" | \"bottom-left\";\n  freeAttempts?: number;\n}\n\nexport function ScratchCardPopup({\n  theme = \"ecommerce\",\n  asPopup = false,\n  trigger = \"manual\",\n  delaySeconds = 4,\n  widgetPosition = \"bottom-right\",\n  freeAttempts = 3,\n  className,\n  style,\n  ...props\n}: ScratchCardPopupProps) {\n  const uid = React.useId();\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const isDrawing = React.useRef(false);\n  const lastPoint = React.useRef<{ x: number; y: number } | null>(null);\n  const copyTimerRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n  const [popupOpen, setPopupOpen] = React.useState(!asPopup);\n  const [scrollTriggered, setScrollTriggered] = React.useState(false);\n  const [roundId, setRoundId] = React.useState(0);\n  const [isRevealed, setIsRevealed] = React.useState(false);\n  const [scratchProgress, setScratchProgress] = React.useState(0);\n  const [awardedPrize, setAwardedPrize] = React.useState<Prize | null>(null);\n  const [attemptsLeft, setAttemptsLeft] = React.useState(freeAttempts);\n  const [rewards, setRewards] = React.useState<Prize[]>([]);\n  const [copiedCode, setCopiedCode] = React.useState<string | null>(null);\n  const [showConfetti, setShowConfetti] = React.useState(false);\n\n  const activeTheme = THEMES[theme] || THEMES.ecommerce;\n  const cardSize = asPopup ? 300 : 270;\n  const cardRadius = 30;\n  const itemRadius = 24;\n\n  // Shuffled only after mount — shuffling during render would make the\n  // server-rendered order (Math.random() has no seed) mismatch the client's,\n  // triggering a hydration error.\n  const [displayItems, setDisplayItems] = React.useState(DEFAULT_IMAGES);\n  React.useEffect(() => {\n    setDisplayItems(shuffle(DEFAULT_IMAGES));\n  }, [roundId]);\n  const matchedIndices = React.useMemo(() => {\n    const counts: Record<string, number> = {};\n    const indices: Record<string, number[]> = {};\n    displayItems.forEach((src, idx) => {\n      counts[src] = (counts[src] || 0) + 1;\n      (indices[src] ||= []).push(idx);\n    });\n    for (const [key, count] of Object.entries(counts)) {\n      if (count >= 3) return indices[key];\n    }\n    return null;\n  }, [displayItems]);\n  const isWon = isRevealed && matchedIndices !== null;\n\n  React.useEffect(() => () => clearTimeout(copyTimerRef.current), []);\n  React.useEffect(() => setPopupOpen(!asPopup), [asPopup]);\n\n  React.useEffect(() => {\n    if (!asPopup || trigger !== \"delay\") return;\n    const t = setTimeout(() => setPopupOpen(true), delaySeconds * 1000);\n    return () => clearTimeout(t);\n  }, [asPopup, trigger, delaySeconds]);\n\n  React.useEffect(() => {\n    if (!asPopup || trigger !== \"scroll\" || !rootRef.current) return;\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting && !scrollTriggered) {\n          setScrollTriggered(true);\n          setPopupOpen(true);\n        }\n      },\n      { threshold: 0.4 },\n    );\n    observer.observe(rootRef.current);\n    return () => observer.disconnect();\n  }, [asPopup, trigger, scrollTriggered]);\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas || isRevealed) return;\n    drawFoil(canvas, cardSize);\n  }, [isRevealed, roundId, cardSize]);\n\n  const getPos = (e: React.MouseEvent | React.TouchEvent) => {\n    const canvas = canvasRef.current;\n    if (!canvas) return { x: 0, y: 0 };\n    const rect = canvas.getBoundingClientRect();\n    const isTouch = \"touches\" in e;\n    const clientX = isTouch ? e.touches[0].clientX : (e as React.MouseEvent).clientX;\n    const clientY = isTouch ? e.touches[0].clientY : (e as React.MouseEvent).clientY;\n    return { x: clientX - rect.left, y: clientY - rect.top };\n  };\n\n  const getFilledPercentage = () => {\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!canvas || !ctx) return 0;\n    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n    const data = imageData.data;\n    let transparent = 0;\n    const step = 32;\n    for (let i = 3; i < data.length; i += 4 * step) {\n      if (data[i] < 40) transparent++;\n    }\n    return Math.round((transparent / (data.length / 4 / step)) * 100);\n  };\n\n  const completeReveal = () => {\n    if (isRevealed || attemptsLeft <= 0) return;\n    setIsRevealed(true);\n    setScratchProgress(100);\n    if (matchedIndices === null) return;\n\n    const lastCode = rewards[rewards.length - 1]?.code;\n    const pool = activeTheme.prizes.length > 1 ? activeTheme.prizes.filter((p) => p.code !== lastCode) : activeTheme.prizes;\n    const prize = pool[Math.floor(Math.random() * pool.length)];\n    setRewards((r) => [...r, prize]);\n    setAttemptsLeft((n) => n - 1);\n    setAwardedPrize(prize);\n    setShowConfetti(true);\n  };\n\n  const scratchAgain = () => {\n    if (attemptsLeft <= 0) return;\n    setIsRevealed(false);\n    setScratchProgress(0);\n    setAwardedPrize(null);\n    setShowConfetti(false);\n    setRoundId((id) => id + 1);\n  };\n\n  const handleStart = (e: React.MouseEvent | React.TouchEvent) => {\n    if (attemptsLeft <= 0) return;\n    isDrawing.current = true;\n    lastPoint.current = getPos(e);\n  };\n\n  const handleMove = (e: React.MouseEvent | React.TouchEvent) => {\n    if (!isDrawing.current || !lastPoint.current || attemptsLeft <= 0) return;\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!canvas || !ctx) return;\n\n    const current = getPos(e);\n    const dist = Math.hypot(current.x - lastPoint.current.x, current.y - lastPoint.current.y);\n    const angle = Math.atan2(current.x - lastPoint.current.x, current.y - lastPoint.current.y);\n\n    ctx.globalCompositeOperation = \"destination-out\";\n    for (let i = 0; i < dist; i += 2.5) {\n      const x = lastPoint.current.x + Math.sin(angle) * i;\n      const y = lastPoint.current.y + Math.cos(angle) * i;\n      ctx.beginPath();\n      ctx.arc(x, y, BRUSH_SIZE / 2, 0, Math.PI * 2);\n      ctx.fill();\n    }\n\n    lastPoint.current = current;\n    const percent = getFilledPercentage();\n    setScratchProgress(percent);\n    if (percent >= 42) completeReveal();\n  };\n\n  const handleEnd = () => {\n    isDrawing.current = false;\n    lastPoint.current = null;\n  };\n\n  const copyRewardCode = (code: string) => {\n    navigator.clipboard.writeText(code).catch(() => {});\n    setCopiedCode(code);\n    clearTimeout(copyTimerRef.current);\n    copyTimerRef.current = setTimeout(() => setCopiedCode((c) => (c === code ? null : c)), 1500);\n  };\n\n  const confettiColors = [activeTheme.accent, \"#f59e0b\", \"#22c55e\", \"#0f172a\", activeTheme.accent];\n\n  const gameCard = (\n    <div className={cn(\"relative box-border flex w-full flex-col items-center overflow-hidden rounded-[36px]\", asPopup ? \"p-7\" : \"p-4\")} style={{ background: activeTheme.primary, color: \"#0f172a\", fontFamily: \"Inter, system-ui, -apple-system, sans-serif\" }}>\n      <div className=\"pointer-events-none absolute top-[-60px] left-1/2 -translate-x-1/2\" style={{ width: cardSize * 1.2, height: cardSize * 1.2, background: `radial-gradient(circle, ${activeTheme.accent}18 0%, transparent 70%)` }} />\n\n      {showConfetti && (\n        <div className=\"pointer-events-none absolute inset-0 overflow-hidden\">\n          <ScratchConfetti colors={confettiColors} onDone={() => setShowConfetti(false)} />\n        </div>\n      )}\n\n      <div className={cn(\"w-full text-center\", asPopup ? \"mb-5.5\" : \"mb-4\")}>\n        <h2 id={`${uid}-title`} className=\"m-0 text-[23px] font-semibold tracking-tight\">\n          {activeTheme.title}\n        </h2>\n        <p className=\"mt-1.5 text-sm leading-snug\" style={{ color: \"rgba(15,23,42,0.55)\" }}>\n          {activeTheme.subtitle}\n        </p>\n        <div className=\"mt-2.5 inline-block rounded-full px-3 py-1 text-[11px] font-bold\" style={{ background: `${activeTheme.accent}12`, color: activeTheme.accent }}>\n          {attemptsLeft > 0 ? `${attemptsLeft} scratch${attemptsLeft > 1 ? \"es\" : \"\"} left` : \"No scratches left\"}\n        </div>\n      </div>\n\n      <div className=\"relative shrink-0 overflow-hidden rounded-[30px] bg-white shadow-[0_18px_40px_-12px_rgba(0,0,0,0.55)]\" style={{ width: cardSize, height: cardSize }}>\n        <div className=\"absolute inset-0 grid grid-cols-3 gap-1.5 p-3.5\">\n          {displayItems.map((src, i) => {\n            const isMatch = isWon && matchedIndices !== null && matchedIndices.includes(i);\n            return (\n              <div\n                key={i}\n                className=\"flex items-center justify-center rounded-xl\"\n                style={{\n                  background: isMatch ? `${activeTheme.accent}15` : \"rgba(15,23,42,0.03)\",\n                  borderRadius: itemRadius,\n                  border: isMatch ? `1.5px solid ${activeTheme.accent}` : \"1px solid rgba(15,23,42,0.06)\",\n                  boxShadow: isMatch ? `0 0 16px ${activeTheme.accent}30` : \"none\",\n                }}\n              >\n                {/* eslint-disable-next-line @next/next/no-img-element */}\n                <img src={src} alt=\"\" className=\"h-[58%] w-[58%] object-contain\" />\n              </div>\n            );\n          })}\n        </div>\n\n        <AnimatePresence>\n          {!isRevealed && (\n            <motion.canvas\n              ref={canvasRef}\n              aria-hidden=\"true\"\n              initial={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.4 }}\n              className=\"absolute inset-0 h-full w-full rounded-[24px]\"\n              style={{ cursor: \"crosshair\", touchAction: \"none\" }}\n              onMouseDown={handleStart}\n              onMouseMove={handleMove}\n              onMouseUp={handleEnd}\n              onMouseLeave={handleEnd}\n              onTouchStart={handleStart}\n              onTouchMove={handleMove}\n              onTouchEnd={handleEnd}\n            />\n          )}\n        </AnimatePresence>\n      </div>\n\n      {!isRevealed && (\n        <div className={asPopup ? \"mt-4.5\" : \"mt-3.5\"} style={{ width: cardSize }}>\n          <div role=\"progressbar\" aria-valuenow={scratchProgress} aria-valuemin={0} aria-valuemax={100} className=\"h-[3px] overflow-hidden rounded-full\" style={{ background: \"rgba(15,23,42,0.1)\" }}>\n            <motion.div className=\"h-full rounded-full\" style={{ background: activeTheme.accent }} animate={{ width: `${scratchProgress}%` }} />\n          </div>\n          <div className=\"mt-1.5 flex justify-between text-[11px]\" style={{ color: \"rgba(15,23,42,0.55)\" }}>\n            <span>Scratch progress</span>\n            <span>{scratchProgress}%</span>\n          </div>\n        </div>\n      )}\n\n      {!isRevealed && attemptsLeft > 0 && (\n        <button onClick={completeReveal} className=\"mt-3 cursor-pointer border-none bg-transparent text-xs underline\" style={{ color: \"rgba(15,23,42,0.55)\" }}>\n          Reveal instantly\n        </button>\n      )}\n\n      <AnimatePresence>\n        {isRevealed && isWon && awardedPrize && (\n          <motion.div role=\"status\" aria-live=\"polite\" initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} className=\"mt-4.5 w-full rounded-3xl border border-black/8 bg-white p-5 text-center shadow-[0_8px_24px_-12px_rgba(15,23,42,0.15)]\" style={{ maxWidth: cardSize + 10 }}>\n            <div className=\"mb-1 text-[11px] tracking-wide uppercase\" style={{ color: activeTheme.accent }}>\n              {awardedPrize.title}\n            </div>\n            <div className=\"mb-3.5 text-sm\" style={{ color: \"rgba(15,23,42,0.55)\" }}>\n              {awardedPrize.description}\n            </div>\n            <div className=\"flex gap-2\">\n              <div className=\"flex-1 rounded-[10px] py-2.5 text-[15px] font-semibold\" style={{ background: `${activeTheme.accent}12`, border: `1px solid ${activeTheme.accent}44`, color: activeTheme.accent }}>\n                {awardedPrize.code}\n              </div>\n              <button onClick={() => copyRewardCode(awardedPrize.code)} className=\"cursor-pointer rounded-[10px] border-none px-3.5 text-[13px] font-semibold text-white\" style={{ background: activeTheme.accent }}>\n                {copiedCode === awardedPrize.code ? \"Copied\" : \"Copy\"}\n              </button>\n            </div>\n            {attemptsLeft > 0 && (\n              <button onClick={scratchAgain} className=\"mt-3 w-full cursor-pointer rounded-[10px] py-2.5 text-[13px] font-semibold\" style={{ border: `1px solid ${activeTheme.accent}33`, color: activeTheme.accent }}>\n                Scratch Again ({attemptsLeft} left)\n              </button>\n            )}\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      {rewards.length > 0 && (\n        <div className=\"mt-4.5 w-full\" style={{ maxWidth: cardSize + 10 }}>\n          <div className=\"mb-2.5 text-center text-xs font-bold\" style={{ color: \"rgba(15,23,42,0.55)\" }}>\n            Your Rewards\n          </div>\n          <div className=\"flex flex-col gap-2\">\n            {rewards.map((r, i) => (\n              <div key={i} className=\"flex items-center justify-between gap-2.5 rounded-xl border border-black/8 bg-white px-3.5 py-2.5\">\n                <div>\n                  <div className=\"text-[13px] font-bold\">{r.description}</div>\n                  <div className=\"mt-0.5 text-xs font-semibold\" style={{ color: activeTheme.accent }}>\n                    {r.code}\n                  </div>\n                </div>\n                <button onClick={() => copyRewardCode(r.code)} className=\"shrink-0 cursor-pointer rounded-lg border-none px-2.5 py-1.5 text-[11px] font-semibold\" style={{ background: `${activeTheme.accent}12`, color: activeTheme.accent }}>\n                  {copiedCode === r.code ? \"Copied\" : \"Copy\"}\n                </button>\n              </div>\n            ))}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n\n  if (!asPopup) {\n    return (\n      <div ref={rootRef} className={className} style={style} {...props}>\n        {gameCard}\n      </div>\n    );\n  }\n\n  return (\n    <div ref={rootRef} className={className} style={style} {...props}>\n      <div className={cn(\"fixed bottom-6 z-[9998] flex items-center\", widgetPosition === \"bottom-left\" ? \"left-6\" : \"right-6\")}>\n        <motion.button\n          onClick={() => setPopupOpen(true)}\n          whileHover={{ scale: 1.04 }}\n          whileTap={{ scale: 0.97 }}\n          aria-label={activeTheme.triggerText}\n          className=\"flex h-14 items-center gap-3 rounded-full border border-black/8 bg-white py-1.5 pr-5 pl-1.5 shadow-[0_8px_30px_rgba(0,0,0,0.18)]\"\n          style={{ fontFamily: \"Inter, system-ui, sans-serif\" }}\n        >\n          <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-full\" style={{ background: activeTheme.primary }}>\n            <GiftIcon />\n          </span>\n          <span className=\"text-[13px] font-bold whitespace-nowrap uppercase\" style={{ color: activeTheme.accent }}>\n            {activeTheme.triggerText}\n          </span>\n        </motion.button>\n      </div>\n\n      <AnimatePresence>\n        {popupOpen && (\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"fixed inset-0 z-[9999] flex items-center justify-center p-5\"\n            style={{ background: \"rgba(0,0,0,0.7)\", backdropFilter: \"blur(4px)\" }}\n          >\n            <motion.div\n              initial={{ scale: 0.94, y: 16 }}\n              animate={{ scale: 1, y: 0 }}\n              exit={{ scale: 0.96, opacity: 0 }}\n              transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}\n              role=\"dialog\"\n              aria-modal=\"true\"\n              aria-labelledby={`${uid}-title`}\n              tabIndex={-1}\n              onKeyDown={(e) => {\n                if (e.key === \"Escape\") setPopupOpen(false);\n              }}\n              className=\"relative w-full max-w-[420px]\"\n              style={{ maxHeight: \"90vh\" }}\n            >\n              {gameCard}\n              <button\n                onClick={() => setPopupOpen(false)}\n                aria-label=\"Close\"\n                className=\"absolute -top-3.5 -right-3.5 z-[100] flex h-8 w-8 items-center justify-center rounded-full border border-black/10 bg-white text-[17px] text-black shadow-[0_4px_16px_rgba(0,0,0,0.25)]\"\n              >\n                ×\n              </button>\n            </motion.div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}