{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cookie-consent",
  "title": "Cookie Consent",
  "description": "Dismissible cookie-consent banner with positional variants and accept / reject actions for privacy compliance.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/cookie-consent/cookie-consent.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, useCallback, useEffect, useState } from \"react\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Button } from \"@vllnt/ui\";\n\nconst cookieConsentVariants = cva(\n  // Base: safe-area-inset for notched devices\n  // Desktop: compact single-line layout, height matches button\n  \"fixed z-50 rounded-lg border bg-background shadow-lg transition-all duration-300 max-w-[calc(100vw-2rem)] p-4 sm:py-1.5 sm:px-3\",\n  {\n    defaultVariants: {\n      position: \"bottom-left\",\n    },\n    variants: {\n      position: {\n        // Mobile: 16px (1rem) from edges, respects safe area\n        // Desktop: 16px from edges\n        \"bottom-center\": \"bottom-4 left-1/2 -translate-x-1/2 mb-safe\",\n        \"bottom-left\": \"bottom-4 left-4 right-4 sm:right-auto mb-safe ml-safe\",\n        \"bottom-right\": \"bottom-4 right-4 left-4 sm:left-auto mb-safe mr-safe\",\n      },\n    },\n  },\n);\n\nexport type CookieConsentProps = {\n  /** Text for the accept button */\n  acceptText?: string;\n  /** Text for the decline button (optional, hidden if not provided) */\n  declineText?: string;\n  /** Text to display in the banner */\n  message?: string;\n  /** Called when user accepts */\n  onAccept?: () => void;\n  /** Called when user declines */\n  onDecline?: () => void;\n  /** Called when visibility changes */\n  onOpenChange?: (open: boolean) => void;\n  /** Whether the banner is visible */\n  open?: boolean;\n  /** URL for privacy policy / settings page */\n  settingsHref?: string;\n  /** Text for the settings/learn more link */\n  settingsText?: string;\n  /** Whether to show the close button */\n  showCloseButton?: boolean;\n} & VariantProps<typeof cookieConsentVariants> &\n  Omit<React.HTMLAttributes<HTMLDivElement>, \"children\">;\n\n/**\n * Cookie consent banner component (Vercel-style)\n *\n * Positioned in bottom-left by default with minimal, clean design.\n * Shows accept button prominently, with optional decline and settings link.\n */\nconst CookieConsent = forwardRef<HTMLDivElement, CookieConsentProps>(\n  (\n    {\n      acceptText = \"Accept\",\n      className,\n      declineText,\n      message = \"This site uses cookies to improve your experience.\",\n      onAccept,\n      onDecline,\n      onOpenChange,\n      open = true,\n      position,\n      settingsHref,\n      settingsText = \"Learn more\",\n      showCloseButton = false,\n      ...props\n    },\n    reference,\n  ) => {\n    const [isVisible, setIsVisible] = useState(false);\n    const [isAnimatingOut, setIsAnimatingOut] = useState(false);\n\n    // Handle visibility with animation\n    useEffect(() => {\n      if (open) {\n        // Small delay for mount animation\n        const timer = setTimeout(() => {\n          setIsVisible(true);\n        }, 50);\n        return () => {\n          clearTimeout(timer);\n        };\n      }\n      const rafId = requestAnimationFrame(() => {\n        setIsVisible(false);\n      });\n      return () => {\n        cancelAnimationFrame(rafId);\n      };\n    }, [open]);\n\n    const handleClose = useCallback(() => {\n      setIsAnimatingOut(true);\n      setTimeout(() => {\n        setIsAnimatingOut(false);\n        onOpenChange?.(false);\n      }, 200);\n    }, [onOpenChange]);\n\n    const handleAccept = useCallback(() => {\n      onAccept?.();\n      handleClose();\n    }, [onAccept, handleClose]);\n\n    const handleDecline = useCallback(() => {\n      onDecline?.();\n      handleClose();\n    }, [onDecline, handleClose]);\n\n    if (!open && !isAnimatingOut) return null;\n\n    return (\n      <div\n        aria-label=\"Cookie consent\"\n        aria-live=\"polite\"\n        className={cn(\n          cookieConsentVariants({ position }),\n          // Animation states\n          isVisible && !isAnimatingOut\n            ? \"translate-y-0 opacity-100\"\n            : \"translate-y-4 opacity-0\",\n          className,\n        )}\n        ref={reference}\n        role=\"dialog\"\n        {...props}\n      >\n        {/* Mobile: stacked layout */}\n        <div className=\"flex flex-col gap-3 sm:hidden\">\n          <p className=\"text-sm text-muted-foreground\">{message}</p>\n          <div className=\"flex items-center gap-2\">\n            {settingsHref ? (\n              <a\n                className=\"text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground\"\n                href={settingsHref}\n              >\n                {settingsText}\n              </a>\n            ) : null}\n            {declineText ? (\n              <Button\n                onClick={handleDecline}\n                size=\"sm\"\n                type=\"button\"\n                variant=\"ghost\"\n              >\n                {declineText}\n              </Button>\n            ) : null}\n            <Button\n              onClick={handleAccept}\n              size=\"sm\"\n              type=\"button\"\n              variant=\"default\"\n            >\n              {acceptText}\n            </Button>\n          </div>\n        </div>\n\n        {/* Desktop: single line, all inline */}\n        <div className=\"hidden sm:flex sm:items-center sm:gap-3\">\n          <p className=\"text-sm text-muted-foreground\">{message}</p>\n          {settingsHref ? (\n            <a\n              className=\"text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground whitespace-nowrap\"\n              href={settingsHref}\n            >\n              {settingsText}\n            </a>\n          ) : null}\n          {declineText ? (\n            <Button\n              className=\"h-7 px-3 text-xs\"\n              onClick={handleDecline}\n              type=\"button\"\n              variant=\"ghost\"\n            >\n              {declineText}\n            </Button>\n          ) : null}\n          <Button\n            className=\"h-7 px-3 text-xs\"\n            onClick={handleAccept}\n            type=\"button\"\n            variant=\"default\"\n          >\n            {acceptText}\n          </Button>\n        </div>\n\n        {showCloseButton ? (\n          <button\n            aria-label=\"Close cookie consent\"\n            className=\"absolute -right-2 -top-2 rounded-full border bg-background p-1 text-muted-foreground hover:text-foreground\"\n            onClick={handleClose}\n            type=\"button\"\n          >\n            <X className=\"size-3\" />\n          </button>\n        ) : null}\n      </div>\n    );\n  },\n);\nCookieConsent.displayName = \"CookieConsent\";\n\nexport { CookieConsent, cookieConsentVariants };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.2.1",
  "stability": "stable"
}
