{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "promo-banner",
  "type": "registry:ui",
  "title": "Promo Banner",
  "description": "Floating promotional banner with marquee animation, discount badges, and localStorage persistence.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/promo-banner.tsx",
      "content": "\"use client\"\n\nimport { Star, X } from \"lucide-react\"\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\n// ============================================================================\n// Marquee Component\n// ============================================================================\n\ninterface MarqueeProps {\n  children: React.ReactNode\n  className?: string\n  speed?: number\n  pauseOnHover?: boolean\n}\n\nfunction Marquee({\n  children,\n  className,\n  speed = 20,\n  pauseOnHover = true,\n}: MarqueeProps) {\n  return (\n    <div\n      className={cn(\n        \"flex overflow-hidden [--duration:20s]\",\n        pauseOnHover && \"hover:[animation-play-state:paused]\",\n        className,\n      )}\n      style={{ \"--duration\": `${speed}s` } as React.CSSProperties}\n    >\n      <div className=\"flex min-w-full shrink-0 animate-marquee items-center justify-around gap-4\">\n        {children}\n      </div>\n      <div\n        className=\"flex min-w-full shrink-0 animate-marquee items-center justify-around gap-4\"\n        aria-hidden=\"true\"\n      >\n        {children}\n      </div>\n    </div>\n  )\n}\n\n// ============================================================================\n// PromoBanner Component\n// ============================================================================\n\ninterface PromoBannerProps {\n  /** Campaign or promo title */\n  title?: string\n  /** Main headline text or element */\n  headline: React.ReactNode\n  /** Current price (optional) */\n  price?: number\n  /** Original price (crossed out) */\n  originalPrice?: number\n  /** Currency symbol */\n  currency?: string\n  /** Call-to-action button text or element */\n  ctaText?: React.ReactNode\n  /** Call-to-action href */\n  ctaHref?: string\n  /** Text for the marquee banner */\n  marqueeText?: string\n  /** Position of the banner */\n  position?: \"bottom-right\" | \"bottom-left\" | \"top-right\" | \"top-left\"\n  /** Product images to display */\n  images?: string[]\n  /** Custom gradient colors */\n  gradientFrom?: string\n  gradientTo?: string\n  /** Close callback */\n  onClose?: () => void\n  /** LocalStorage key for persistence */\n  storageKey?: string\n  /** Delay before showing (ms) */\n  delay?: number\n  /** Show by default or wait for trigger */\n  defaultOpen?: boolean\n  /** Show diagonal lines pattern on background */\n  showPattern?: boolean\n  /** Callback when CTA button is clicked */\n  onCtaClick?: () => void\n  /** Custom className */\n  className?: string\n}\n\nexport function PromoBanner({\n  title = \"Special Offer\",\n  headline,\n  price,\n  originalPrice,\n  currency = \"$\",\n  ctaText = \"Get Now\",\n  ctaHref = \"#\",\n  marqueeText,\n  position = \"bottom-right\",\n  images,\n  gradientFrom = \"from-violet-600\",\n  gradientTo = \"to-pink-600\",\n  onClose,\n  storageKey,\n  delay = 0,\n  defaultOpen = true,\n  showPattern = false,\n  onCtaClick,\n  className,\n}: PromoBannerProps) {\n  const [isVisible, setIsVisible] = React.useState(false)\n  const [isClosing, setIsClosing] = React.useState(false)\n  const [isEntering, setIsEntering] = React.useState(true)\n\n  React.useEffect(() => {\n    // Check localStorage if storageKey is provided\n    if (storageKey) {\n      const dismissed = localStorage.getItem(storageKey)\n      if (dismissed === \"true\") {\n        return\n      }\n    }\n\n    // Show with delay\n    if (defaultOpen) {\n      const timer = setTimeout(() => {\n        setIsVisible(true)\n        // Trigger entrance animation after a frame\n        requestAnimationFrame(() => {\n          requestAnimationFrame(() => {\n            setIsEntering(false)\n          })\n        })\n      }, delay)\n\n      return () => clearTimeout(timer)\n    }\n  }, [storageKey, delay, defaultOpen])\n\n  const handleClose = React.useCallback(() => {\n    setIsClosing(true)\n\n    // Store dismissal in localStorage\n    if (storageKey) {\n      localStorage.setItem(storageKey, \"true\")\n    }\n\n    // Wait for animation to complete\n    setTimeout(() => {\n      setIsVisible(false)\n      setIsClosing(false)\n      onClose?.()\n    }, 300)\n  }, [storageKey, onClose])\n\n  if (!isVisible) return null\n\n  const positionClasses = {\n    \"bottom-right\": \"bottom-4 right-4\",\n    \"bottom-left\": \"bottom-4 left-4\",\n    \"top-right\": \"top-4 right-4\",\n    \"top-left\": \"top-4 left-4\",\n  }\n\n  const discount =\n    originalPrice && price !== undefined\n      ? Math.round(((originalPrice - price) / originalPrice) * 100)\n      : null\n\n  return (\n    <div\n      className={cn(\n        \"fixed z-50 w-[22rem] overflow-hidden rounded-2xl shadow-2xl\",\n        \"transition-all duration-500 ease-out\",\n        isClosing || isEntering\n          ? \"translate-y-8 opacity-0 scale-95\"\n          : \"translate-y-0 opacity-100 scale-100\",\n        positionClasses[position],\n        className,\n      )}\n      role=\"dialog\"\n      aria-label=\"Promotional banner\"\n    >\n      {/* Main Content */}\n      <div\n        className={cn(\n          \"relative bg-gradient-to-br p-6 overflow-hidden\",\n          gradientFrom,\n          gradientTo,\n        )}\n      >\n        {/* Diagonal Pattern Overlay */}\n        {showPattern && (\n          <div\n            className=\"pointer-events-none absolute inset-0 z-0 opacity-[0.08]\"\n            style={{\n              backgroundImage: `repeating-linear-gradient(\n                -45deg,\n                transparent,\n                transparent 6px,\n                rgba(255, 255, 255, 1) 6px,\n                rgba(255, 255, 255, 1) 9px\n              )`,\n            }}\n          />\n        )}\n        {/* Close Button */}\n        <button\n          onClick={handleClose}\n          className=\"absolute right-3 top-3 z-10 rounded-full bg-white/20 p-1.5 text-white/80 backdrop-blur-sm transition-colors hover:bg-white/30 hover:text-white cursor-pointer\"\n          aria-label=\"Close banner\"\n        >\n          <X className=\"h-4 w-4\" />\n        </button>\n\n        {/* Discount Badge */}\n        {discount && (\n          <div className=\"absolute left-4 top-4 z-10\">\n            <span className=\"rounded-full bg-yellow-400 px-2.5 py-1 text-xs font-bold text-black\">\n              -{discount}%\n            </span>\n          </div>\n        )}\n\n        {/* Content */}\n        <div className=\"relative z-10 mt-4 text-center text-white\">\n          {/* Title */}\n          <p className=\"text-xs font-medium uppercase tracking-widest text-white/70\">\n            {title}\n          </p>\n\n          {/* Headline */}\n          <h3 className=\"mt-2 text-xl font-bold leading-tight\">{headline}</h3>\n\n          {/* Price Section */}\n          {price !== undefined && (\n            <div className=\"mt-4 flex items-baseline justify-center gap-2\">\n              <span className=\"text-3xl font-bold\">\n                {currency}\n                {price}\n              </span>\n              {originalPrice && (\n                <span className=\"text-lg text-white/60 line-through\">\n                  {currency}\n                  {originalPrice}\n                </span>\n              )}\n            </div>\n          )}\n\n          {/* Product Images */}\n          {images && images.length > 0 && (\n            <div className=\"mt-4 flex justify-center gap-2\">\n              {images.slice(0, 3).map((src, index) => (\n                <div\n                  key={index}\n                  className=\"h-14 w-14 overflow-hidden rounded-lg bg-white/10 backdrop-blur-sm\"\n                >\n                  <img\n                    src={src}\n                    alt=\"\"\n                    className=\"h-full w-full object-cover\"\n                  />\n                </div>\n              ))}\n            </div>\n          )}\n\n          {/* CTA Button */}\n          <a\n            href={ctaHref}\n            onClick={onCtaClick}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"mt-5 inline-block rounded-full bg-white px-6 py-2.5 text-sm font-semibold text-gray-900 shadow-lg transition-all hover:scale-[1.02] active:scale-[0.98] cursor-pointer\"\n          >\n            {ctaText}\n          </a>\n        </div>\n      </div>\n\n      {/* Marquee */}\n      {marqueeText && (\n        <div className=\"bg-black/90 py-2 text-white\">\n          <Marquee speed={15}>\n            {marqueeText.split(\" • \").map((phrase, index) => (\n              <React.Fragment key={index}>\n                <span className=\"text-xs font-medium uppercase tracking-wide whitespace-nowrap\">\n                  {phrase}\n                </span>\n                <Star className=\"mx-3 h-3 w-3 shrink-0 fill-yellow-400 text-yellow-400\" />\n              </React.Fragment>\n            ))}\n          </Marquee>\n        </div>\n      )}\n    </div>\n  )\n}\n\n// ============================================================================\n// PromoBanner Provider (for global management)\n// ============================================================================\n\ninterface PromoBannerConfig extends Omit<PromoBannerProps, \"onClose\"> {\n  expiresAt?: Date\n}\n\ninterface PromoBannerContextValue {\n  show: (config: PromoBannerConfig) => void\n  hide: () => void\n  isVisible: boolean\n}\n\nconst PromoBannerContext = React.createContext<PromoBannerContextValue | null>(\n  null,\n)\n\nexport function usePromoBanner() {\n  const context = React.useContext(PromoBannerContext)\n  if (!context) {\n    throw new Error(\"usePromoBanner must be used within PromoBannerProvider\")\n  }\n  return context\n}\n\ninterface PromoBannerProviderProps {\n  children: React.ReactNode\n  initialConfig?: PromoBannerConfig\n}\n\nexport function PromoBannerProvider({\n  children,\n  initialConfig,\n}: PromoBannerProviderProps) {\n  const [config, setConfig] = React.useState<PromoBannerConfig | null>(\n    initialConfig || null,\n  )\n  const [isVisible, setIsVisible] = React.useState(false)\n\n  React.useEffect(() => {\n    if (config) {\n      // Check if expired\n      if (config.expiresAt && new Date() > config.expiresAt) {\n        return\n      }\n      setIsVisible(true)\n    }\n  }, [config])\n\n  const show = React.useCallback((newConfig: PromoBannerConfig) => {\n    setConfig(newConfig)\n    setIsVisible(true)\n  }, [])\n\n  const hide = React.useCallback(() => {\n    setIsVisible(false)\n    setConfig(null)\n  }, [])\n\n  return (\n    <PromoBannerContext.Provider value={{ show, hide, isVisible }}>\n      {children}\n      {config && isVisible && (\n        <PromoBanner {...config} onClose={hide} defaultOpen={true} />\n      )}\n    </PromoBannerContext.Provider>\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}