{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sales-ticket-popup",
  "title": "Sales Ticket Popup",
  "description": "A corner-anchored promo popup shaped like a torn event ticket, with 5 themes, a zigzag badge stub, immediate/delay/scroll triggers, auto-hide, and dismiss-memory.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/new-york/sales-ticket-popup/sales-ticket-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\nexport type SalesTicketTheme = \"dark\" | \"light\" | \"gradient\" | \"neon\" | \"outline\";\nexport type SalesTicketCorner = \"bottom-right\" | \"bottom-left\" | \"top-right\" | \"top-left\";\nexport type SalesTicketTrigger = \"immediate\" | \"delay\" | \"scroll\";\nexport type SalesTicketAnimationStyle = \"slide\" | \"scale\" | \"bounce\";\n\nexport interface SalesTicketPopupProps {\n  fixed?: boolean;\n  emoji?: string;\n  kicker?: string;\n  badgeText?: string;\n  description?: string;\n  buttonLabel?: string;\n  buttonHref?: string;\n  onButtonClick?: () => void;\n  onClose?: () => void;\n  theme?: SalesTicketTheme;\n  accent?: string;\n  radius?: number;\n  texture?: boolean;\n  corner?: SalesTicketCorner;\n  offsetX?: number;\n  offsetY?: number;\n  maxWidth?: number;\n  trigger?: SalesTicketTrigger;\n  delaySeconds?: number;\n  scrollPercent?: number;\n  animationStyle?: SalesTicketAnimationStyle;\n  dismissible?: boolean;\n  autoHide?: boolean;\n  autoHideSeconds?: number;\n  rememberDismiss?: boolean;\n  dismissKey?: string;\n  className?: string;\n}\n\ninterface Palette {\n  outerBg: string;\n  outerStrong: string;\n  outerMuted: string;\n  outerTexture: string;\n  outerBorder?: string;\n  innerBg: string;\n  innerText: string;\n  innerTexture: string;\n  innerBorder?: string;\n  stubBg: string;\n  stubText: string;\n  stubBorder?: string;\n  stubTicket?: boolean;\n  buttonBg: string;\n  buttonText: string;\n  buttonBorder?: string;\n  shadow: string;\n}\n\nconst THEMES: Record<SalesTicketTheme, Palette> = {\n  dark: {\n    outerBg: \"#0E0E0E\",\n    outerStrong: \"#F5F4F1\",\n    outerMuted: \"rgba(245,244,241,0.55)\",\n    outerTexture: \"rgba(255,255,255,0.04)\",\n    outerBorder: \"1px solid rgba(255,255,255,0.08)\",\n    innerBg: \"#161616\",\n    innerText: \"#F5F4F1\",\n    innerTexture: \"rgba(255,255,255,0.06)\",\n    innerBorder: \"1px solid rgba(255,255,255,0.06)\",\n    stubBg: \"#F5F4F1\",\n    stubText: \"#0A0A0A\",\n    buttonBg: \"#F5F4F1\",\n    buttonText: \"#0A0A0A\",\n    shadow: \"0 28px 70px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.05)\",\n  },\n  light: {\n    outerBg: \"#F5F4F1\",\n    outerStrong: \"#0A0A0A\",\n    outerMuted: \"rgba(10,10,10,0.55)\",\n    outerTexture: \"rgba(0,0,0,0.04)\",\n    outerBorder: \"1px solid rgba(10,10,10,0.06)\",\n    innerBg: \"#FFFFFF\",\n    innerText: \"#0A0A0A\",\n    innerTexture: \"rgba(0,0,0,0.04)\",\n    innerBorder: \"1px solid rgba(10,10,10,0.08)\",\n    stubBg: \"#0A0A0A\",\n    stubText: \"#F5F4F1\",\n    buttonBg: \"#0A0A0A\",\n    buttonText: \"#F5F4F1\",\n    shadow: \"0 28px 70px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.7)\",\n  },\n  gradient: {\n    outerBg: \"#0E0E0E\",\n    outerStrong: \"#F5F4F1\",\n    outerMuted: \"rgba(245,244,241,0.55)\",\n    outerTexture: \"rgba(135,255,227,0.05)\",\n    outerBorder: \"1px solid rgba(135,255,227,0.18)\",\n    innerBg: \"linear-gradient(135deg, #87FFE3 0%, #C4F26B 100%)\",\n    innerText: \"#0A0A0A\",\n    innerTexture: \"rgba(0,0,0,0.1)\",\n    stubBg: \"#0A0A0A\",\n    stubText: \"#87FFE3\",\n    buttonBg: \"linear-gradient(135deg, #87FFE3 0%, #C4F26B 100%)\",\n    buttonText: \"#0A0A0A\",\n    shadow: \"0 28px 70px rgba(135,255,227,0.16), 0 0 0 1px rgba(135,255,227,0.06)\",\n  },\n  neon: {\n    outerBg: \"#0E0E0E\",\n    outerStrong: \"#F5F4F1\",\n    outerMuted: \"rgba(245,244,241,0.55)\",\n    outerTexture: \"rgba(242,168,65,0.05)\",\n    outerBorder: \"1px solid rgba(242,168,65,0.22)\",\n    innerBg: \"#15110A\",\n    innerText: \"#F2A841\",\n    innerTexture: \"rgba(242,168,65,0.1)\",\n    innerBorder: \"1px solid rgba(242,168,65,0.14)\",\n    stubBg: \"#F2A841\",\n    stubText: \"#1A1204\",\n    buttonBg: \"#F2A841\",\n    buttonText: \"#1A1204\",\n    shadow: \"0 28px 70px rgba(242,168,65,0.14), 0 0 0 1px rgba(242,168,65,0.05)\",\n  },\n  outline: {\n    outerBg: \"#0E0E0E\",\n    outerStrong: \"#F5F4F1\",\n    outerMuted: \"rgba(245,244,241,0.55)\",\n    outerTexture: \"transparent\",\n    outerBorder: \"1px solid rgba(245,244,241,0.28)\",\n    innerBg: \"transparent\",\n    innerText: \"#F5F4F1\",\n    innerTexture: \"transparent\",\n    innerBorder: \"1px dashed rgba(245,244,241,0.3)\",\n    stubBg: \"transparent\",\n    stubText: \"#F5F4F1\",\n    stubBorder: \"1px solid rgba(245,244,241,0.45)\",\n    stubTicket: true,\n    buttonBg: \"transparent\",\n    buttonText: \"#F5F4F1\",\n    buttonBorder: \"1px solid rgba(245,244,241,0.45)\",\n    shadow: \"0 28px 70px rgba(0,0,0,0.5)\",\n  },\n};\n\nconst CORNER_STYLE: Record<SalesTicketCorner, React.CSSProperties> = {\n  \"bottom-right\": { bottom: 0, right: 0 },\n  \"bottom-left\": { bottom: 0, left: 0 },\n  \"top-right\": { top: 0, right: 0 },\n  \"top-left\": { top: 0, left: 0 },\n};\n\nconst STUB_HEIGHT = 42;\n\nfunction zigzagPolygon(height: number, teeth = 6, depthPct = 6) {\n  const seg = height / (teeth * 2);\n  const pts: string[] = [\"0% 0px\", \"100% 0px\"];\n  for (let i = 1; i < teeth * 2; i++) {\n    const x = i % 2 === 1 ? `${100 - depthPct}%` : \"100%\";\n    pts.push(`${x} ${i * seg}px`);\n  }\n  pts.push(`100% ${height}px`, `0% ${height}px`);\n  for (let i = teeth * 2 - 1; i >= 1; i--) {\n    const x = i % 2 === 1 ? `${depthPct}%` : \"0%\";\n    pts.push(`${x} ${i * seg}px`);\n  }\n  return `polygon(${pts.join(\",\")})`;\n}\n\nfunction renderRichText(text: string, strongColor: string) {\n  return text.split(/(\\*\\*.*?\\*\\*)/g).map((part, i) =>\n    part.startsWith(\"**\") && part.endsWith(\"**\") ? (\n      <strong key={i} style={{ fontWeight: 800, color: strongColor }}>\n        {part.slice(2, -2)}\n      </strong>\n    ) : (\n      <span key={i}>{part}</span>\n    ),\n  );\n}\n\nfunction NoiseTexture({ color }: { color: string }) {\n  if (color === \"transparent\") return null;\n  return <div className=\"pointer-events-none absolute inset-0\" style={{ backgroundImage: `radial-gradient(${color} 1px, transparent 1px)`, backgroundSize: \"6px 6px\" }} />;\n}\n\nfunction exitOffset(corner: SalesTicketCorner, style: SalesTicketAnimationStyle) {\n  if (style === \"scale\") return { opacity: 0, scale: 0.85 };\n  if (style === \"bounce\") return { opacity: 0, y: 40, scale: 0.9 };\n  const isLeft = corner.includes(\"left\");\n  const isTop = corner.includes(\"top\");\n  return { opacity: 0, x: isLeft ? -60 : 60, y: isTop ? -10 : 10 };\n}\n\nfunction enterTransition(style: SalesTicketAnimationStyle) {\n  if (style === \"bounce\") return { type: \"spring\" as const, stiffness: 320, damping: 16, mass: 0.9 };\n  if (style === \"scale\") return { type: \"spring\" as const, stiffness: 260, damping: 22 };\n  return { type: \"spring\" as const, stiffness: 240, damping: 26 };\n}\n\nfunction TicketCard({\n  emoji,\n  kicker,\n  badgeText,\n  description,\n  buttonLabel,\n  accent,\n  radius,\n  texture,\n  palette,\n  onClose,\n  onButtonClick,\n  buttonHref,\n  dismissible,\n  maxWidth,\n}: {\n  emoji: string;\n  kicker: string;\n  badgeText: string;\n  description: string;\n  buttonLabel: string;\n  accent: string;\n  radius: number;\n  texture: boolean;\n  palette: Palette;\n  onClose: () => void;\n  onButtonClick?: () => void;\n  buttonHref?: string;\n  dismissible: boolean;\n  maxWidth: number;\n}) {\n  const stubBg = accent || palette.stubBg;\n  const buttonBg = accent || palette.buttonBg;\n\n  const handleClick = () => {\n    onButtonClick?.();\n    if (buttonHref) window.open(buttonHref, \"_blank\");\n  };\n\n  return (\n    <div\n      className=\"relative overflow-hidden p-3.5\"\n      style={{\n        width: maxWidth,\n        borderRadius: radius,\n        background: palette.outerBg,\n        boxShadow: palette.shadow,\n        border: palette.outerBorder,\n        fontFamily: \"-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif\",\n      }}\n    >\n      {texture && <NoiseTexture color={palette.outerTexture} />}\n\n      {dismissible && (\n        <button\n          onClick={onClose}\n          aria-label=\"Close\"\n          className=\"absolute top-2.5 right-2.5 z-[2] flex h-6 w-6 items-center justify-center rounded-full border-none bg-[rgba(127,127,127,0.18)] text-xs leading-none\"\n          style={{ color: palette.outerStrong }}\n        >\n          ✕\n        </button>\n      )}\n\n      <div className=\"relative z-[1] overflow-hidden px-4.5 pt-6.5 pb-7.5\" style={{ borderRadius: Math.max(radius - 8, 8), background: palette.innerBg, border: palette.innerBorder }}>\n        {texture && <NoiseTexture color={palette.innerTexture} />}\n\n        <div className=\"relative z-[1] flex items-center justify-center gap-2 text-[26px] font-extrabold tracking-[1px] uppercase\" style={{ color: palette.innerText }}>\n          {emoji && <span className=\"text-[22px]\">{emoji}</span>}\n          {kicker}\n        </div>\n\n        <div\n          className=\"relative z-[1] mx-auto mt-3.5 flex items-center justify-center text-sm font-extrabold whitespace-nowrap\"\n          style={{ width: \"64%\", height: STUB_HEIGHT, background: stubBg, border: palette.stubBorder, clipPath: palette.stubTicket ? undefined : zigzagPolygon(STUB_HEIGHT),\n            borderRadius: palette.stubTicket ? 10 : undefined,\n            position: \"relative\", color: palette.stubText, letterSpacing: 0.3 }}\n        >\n          {badgeText}\n          {palette.stubTicket && (\n            <>\n              <span aria-hidden=\"true\" style={{ position: \"absolute\", top: \"50%\", left: -7, width: 14, height: 14, marginTop: -7, borderRadius: \"50%\", background: palette.outerBg, border: palette.stubBorder, clipPath: \"inset(0 0 0 50%)\" }} />\n              <span aria-hidden=\"true\" style={{ position: \"absolute\", top: \"50%\", right: -7, width: 14, height: 14, marginTop: -7, borderRadius: \"50%\", background: palette.outerBg, border: palette.stubBorder, clipPath: \"inset(0 50% 0 0)\" }} />\n            </>\n          )}\n        </div>\n      </div>\n\n      <div className=\"relative z-[1] px-2 pt-4.5 pb-1.5 text-center\">\n        <div className=\"mb-4 text-[13.5px] leading-[1.5]\" style={{ color: palette.outerMuted }}>\n          {renderRichText(description, palette.outerStrong)}\n        </div>\n        <button onClick={handleClick} className=\"w-full cursor-pointer rounded-full border-none px-[26px] py-[11px] text-[13.5px] font-bold\" style={{ background: buttonBg, color: palette.buttonText, border: palette.buttonBorder }}>\n          {buttonLabel}\n        </button>\n      </div>\n    </div>\n  );\n}\n\nexport function SalesTicketPopup({\n  fixed = true,\n  emoji = \"\",\n  kicker = \"SALE\",\n  badgeText = \"30% OFF\",\n  description = \"Checkout to enjoy **30% off** your order.\",\n  buttonLabel = \"Buy Now\",\n  buttonHref = \"\",\n  onButtonClick,\n  onClose,\n  theme = \"dark\",\n  accent = \"\",\n  radius = 26,\n  texture = true,\n  corner = \"bottom-right\",\n  offsetX = 24,\n  offsetY = 24,\n  maxWidth = 270,\n  trigger = \"delay\",\n  delaySeconds = 2,\n  scrollPercent = 50,\n  animationStyle = \"slide\",\n  dismissible = true,\n  autoHide = false,\n  autoHideSeconds = 8,\n  rememberDismiss = false,\n  dismissKey = \"sales-ticket-dismissed\",\n  className,\n}: SalesTicketPopupProps) {\n  const [visible, setVisible] = React.useState(!fixed);\n  const [dismissed, setDismissed] = React.useState(false);\n  const [inlineClosed, setInlineClosed] = React.useState(false);\n\n  const handleClose = React.useCallback(() => {\n    setVisible(false);\n    onClose?.();\n    if (rememberDismiss) window.localStorage.setItem(dismissKey, \"1\");\n  }, [onClose, rememberDismiss, dismissKey]);\n\n  React.useEffect(() => {\n    if (!fixed) return;\n\n    if (rememberDismiss && window.localStorage.getItem(dismissKey) === \"1\") {\n      const id = setTimeout(() => setDismissed(true), 0);\n      return () => clearTimeout(id);\n    }\n\n    if (trigger === \"immediate\") {\n      const id = setTimeout(() => setVisible(true), 0);\n      return () => clearTimeout(id);\n    }\n\n    if (trigger === \"delay\") {\n      const t = setTimeout(() => setVisible(true), delaySeconds * 1000);\n      return () => clearTimeout(t);\n    }\n\n    if (trigger === \"scroll\") {\n      const handler = () => {\n        const max = document.documentElement.scrollHeight - window.innerHeight;\n        const pct = max > 0 ? (window.scrollY / max) * 100 : 100;\n        if (pct >= scrollPercent) {\n          setVisible(true);\n          window.removeEventListener(\"scroll\", handler);\n        }\n      };\n      window.addEventListener(\"scroll\", handler);\n      return () => window.removeEventListener(\"scroll\", handler);\n    }\n  }, [fixed, trigger, delaySeconds, scrollPercent, rememberDismiss, dismissKey]);\n\n  React.useEffect(() => {\n    if (!fixed || !visible || !autoHide) return;\n    const t = setTimeout(handleClose, autoHideSeconds * 1000);\n    return () => clearTimeout(t);\n  }, [fixed, visible, autoHide, autoHideSeconds, handleClose]);\n\n  const palette = THEMES[theme];\n\n  if (!fixed) {\n    if (inlineClosed) return null;\n    return (\n      <div className={cn(\"inline-block\", className)}>\n        <TicketCard\n          emoji={emoji}\n          kicker={kicker}\n          badgeText={badgeText}\n          description={description}\n          buttonLabel={buttonLabel}\n          accent={accent}\n          radius={radius}\n          texture={texture}\n          palette={palette}\n          onClose={() => {\n            setInlineClosed(true);\n            onClose?.();\n          }}\n          onButtonClick={onButtonClick}\n          buttonHref={buttonHref}\n          dismissible={dismissible}\n          maxWidth={maxWidth}\n        />\n      </div>\n    );\n  }\n\n  if (dismissed) return null;\n\n  return (\n    <div className={cn(\"fixed z-[9999] pointer-events-none\", className)} style={{ ...CORNER_STYLE[corner], margin: `${offsetY}px ${offsetX}px` }}>\n      <AnimatePresence>\n        {visible && (\n          <motion.div\n            className=\"pointer-events-auto\"\n            initial={exitOffset(corner, animationStyle)}\n            animate={{ opacity: 1, x: 0, y: 0, scale: 1 }}\n            exit={exitOffset(corner, animationStyle)}\n            transition={enterTransition(animationStyle)}\n          >\n            <TicketCard\n              emoji={emoji}\n              kicker={kicker}\n              badgeText={badgeText}\n              description={description}\n              buttonLabel={buttonLabel}\n              accent={accent}\n              radius={radius}\n              texture={texture}\n              palette={palette}\n              onClose={handleClose}\n              onButtonClick={onButtonClick}\n              buttonHref={buttonHref}\n              dismissible={dismissible}\n              maxWidth={maxWidth}\n            />\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}