{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "cookie-consent",
  "title": "Cookie Consent",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/cookie-consent/CookieConsentBanner.vue",
      "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { Cookie } from 'lucide-vue-next'\nimport { Button } from '@/components/ui/button'\nimport { Separator } from '@/components/ui/separator'\nimport { Switch } from '@/components/ui/switch'\n\ninterface Props {\n  variant?: 'bottom-bar' | 'card'\n}\n\nwithDefaults(defineProps<Props>(), {\n  variant: 'bottom-bar',\n})\n\nconst visible = ref(true)\nconst showPreferences = ref(false)\nconst analytics = ref(true)\nconst marketing = ref(false)\n\nfunction dismiss() {\n  visible.value = false\n}\n</script>\n\n<template>\n  <Transition\n    enter-active-class=\"transition-opacity duration-150 ease-out\"\n    enter-from-class=\"opacity-0\"\n    leave-active-class=\"transition-opacity duration-150 ease-in\"\n    leave-to-class=\"opacity-0\"\n  >\n    <div v-if=\"visible\" data-slot=\"cookie-consent-banner\" role=\"dialog\" aria-label=\"Cookie consent\">\n      <div v-if=\"variant === 'bottom-bar'\" class=\"bg-card border-border w-full border-t px-4 py-4 sm:px-6\">\n        <div class=\"mx-auto flex max-w-5xl flex-col gap-4\">\n          <div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\">\n            <div class=\"flex items-start gap-3\">\n              <span\n                class=\"bg-primary/10 text-primary mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full\"\n              >\n                <Cookie class=\"size-4\" aria-hidden=\"true\" />\n              </span>\n              <p class=\"text-muted-foreground max-w-xl text-sm leading-relaxed\">\n                We use cookies to improve your experience and analyze traffic. Read our\n                <a\n                  href=\"#\"\n                  class=\"text-foreground focus-visible:ring-ring rounded-sm font-medium underline underline-offset-4 outline-none focus-visible:ring-2\"\n                  >Cookie Policy</a\n                >.\n              </p>\n            </div>\n            <div class=\"flex shrink-0 flex-wrap items-center gap-2\">\n              <Button variant=\"ghost\" size=\"sm\" @click=\"showPreferences = !showPreferences\">\n                {{ showPreferences ? 'Hide preferences' : 'Customize' }}\n              </Button>\n              <Button variant=\"outline\" size=\"sm\" @click=\"dismiss\">Reject</Button>\n              <Button size=\"sm\" @click=\"dismiss\">Accept all</Button>\n            </div>\n          </div>\n\n          <div v-if=\"showPreferences\" class=\"flex flex-col gap-3\">\n            <Separator />\n            <div class=\"flex items-center justify-between gap-4 py-1\">\n              <div>\n                <p class=\"text-sm font-medium\">Necessary</p>\n                <p class=\"text-muted-foreground text-xs\">Required for the site to function.</p>\n              </div>\n              <Switch :model-value=\"true\" disabled aria-label=\"Necessary cookies\" />\n            </div>\n            <div class=\"flex items-center justify-between gap-4 py-1\">\n              <div>\n                <p class=\"text-sm font-medium\">Analytics</p>\n                <p class=\"text-muted-foreground text-xs\">Helps us understand usage patterns.</p>\n              </div>\n              <Switch v-model=\"analytics\" aria-label=\"Analytics cookies\" />\n            </div>\n            <div class=\"flex items-center justify-between gap-4 py-1\">\n              <div>\n                <p class=\"text-sm font-medium\">Marketing</p>\n                <p class=\"text-muted-foreground text-xs\">Used to personalize campaigns.</p>\n              </div>\n              <Switch v-model=\"marketing\" aria-label=\"Marketing cookies\" />\n            </div>\n            <div class=\"mt-1 flex justify-end\">\n              <Button size=\"sm\" @click=\"dismiss\">Save preferences</Button>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div v-else class=\"bg-card border-border mx-auto max-w-md rounded-xl border p-6 shadow-xs\">\n        <div class=\"flex flex-col gap-4\">\n          <div class=\"flex items-start gap-3\">\n            <span class=\"bg-primary/10 text-primary flex size-9 shrink-0 items-center justify-center rounded-full\">\n              <Cookie class=\"size-4\" aria-hidden=\"true\" />\n            </span>\n            <div>\n              <h3 class=\"text-sm font-semibold\">We value your privacy</h3>\n              <p class=\"text-muted-foreground mt-1 text-sm leading-relaxed\">\n                We use cookies to improve your experience and analyze traffic. Read our\n                <a\n                  href=\"#\"\n                  class=\"text-foreground focus-visible:ring-ring rounded-sm font-medium underline underline-offset-4 outline-none focus-visible:ring-2\"\n                  >Cookie Policy</a\n                >.\n              </p>\n            </div>\n          </div>\n\n          <template v-if=\"showPreferences\">\n            <Separator />\n            <div class=\"flex items-center justify-between gap-4 py-1\">\n              <div>\n                <p class=\"text-sm font-medium\">Necessary</p>\n                <p class=\"text-muted-foreground text-xs\">Required for the site to function.</p>\n              </div>\n              <Switch :model-value=\"true\" disabled aria-label=\"Necessary cookies\" />\n            </div>\n            <div class=\"flex items-center justify-between gap-4 py-1\">\n              <div>\n                <p class=\"text-sm font-medium\">Analytics</p>\n                <p class=\"text-muted-foreground text-xs\">Helps us understand usage patterns.</p>\n              </div>\n              <Switch v-model=\"analytics\" aria-label=\"Analytics cookies\" />\n            </div>\n            <div class=\"flex items-center justify-between gap-4 py-1\">\n              <div>\n                <p class=\"text-sm font-medium\">Marketing</p>\n                <p class=\"text-muted-foreground text-xs\">Used to personalize campaigns.</p>\n              </div>\n              <Switch v-model=\"marketing\" aria-label=\"Marketing cookies\" />\n            </div>\n          </template>\n\n          <div class=\"flex flex-wrap items-center gap-2\">\n            <Button variant=\"ghost\" size=\"sm\" @click=\"showPreferences = !showPreferences\">\n              {{ showPreferences ? 'Hide preferences' : 'Customize' }}\n            </Button>\n            <div class=\"ml-auto flex items-center gap-2\">\n              <Button variant=\"outline\" size=\"sm\" @click=\"dismiss\">Reject</Button>\n              <Button size=\"sm\" @click=\"dismiss\">\n                {{ showPreferences ? 'Save preferences' : 'Accept all' }}\n              </Button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </Transition>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/cookie-consent/CookieConsentBanner.vue"
    },
    {
      "path": "packages/registry-vue/blocks/cookie-consent/CookieConsentBar.vue",
      "content": "<script setup lang=\"ts\">\nimport { onMounted, ref } from 'vue'\nimport { Cookie } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Separator } from '@/components/ui/separator'\n\nconst props = withDefaults(defineProps<{ storageKey?: string }>(), {\n  storageKey: 'uipkge:consent',\n})\n\nconst visible = ref(false)\n\nfunction decide(choice: 'accepted' | 'rejected') {\n  visible.value = false\n  try {\n    window.localStorage.setItem(props.storageKey, choice)\n  } catch {\n    // Blocked storage: the choice holds for this page view only.\n  }\n}\n\nonMounted(() => {\n  try {\n    visible.value = !window.localStorage.getItem(props.storageKey)\n  } catch {\n    visible.value = true\n  }\n})\n</script>\n\n<template>\n  <Transition\n    enter-active-class=\"transition duration-200 ease-out\"\n    enter-from-class=\"translate-y-full opacity-0\"\n    leave-active-class=\"transition duration-150 ease-in\"\n    leave-to-class=\"translate-y-full opacity-0\"\n  >\n    <div\n      v-if=\"visible\"\n      data-slot=\"cookie-consent-bar\"\n      class=\"border-border bg-card/95 fixed inset-x-0 bottom-0 z-50 border-t backdrop-blur\"\n      role=\"region\"\n      aria-label=\"Cookie consent\"\n    >\n      <div class=\"mx-auto flex max-w-6xl flex-col gap-4 px-6 py-4 lg:flex-row lg:items-center lg:gap-8\">\n        <div class=\"flex min-w-0 items-start gap-3\">\n          <Cookie class=\"text-muted-foreground mt-0.5 size-4 shrink-0\" aria-hidden=\"true\" />\n          <div class=\"min-w-0\">\n            <div class=\"flex flex-wrap items-center gap-2\">\n              <p class=\"text-sm font-medium\">We store two things</p>\n              <Badge variant=\"outline\">No ad tracking</Badge>\n            </div>\n            <p class=\"text-muted-foreground mt-1 text-sm leading-relaxed\">\n              A session cookie so you stay signed in, and an analytics cookie recording which pages get read. Rejecting\n              keeps the first and drops the second.\n            </p>\n          </div>\n        </div>\n\n        <Separator orientation=\"vertical\" class=\"hidden h-10 lg:block\" />\n\n        <!-- Reject carries the same visual weight as accept. A ghost \"reject\"\n             beside a solid \"accept\" is a dark pattern with extra steps. -->\n        <div class=\"flex shrink-0 flex-wrap items-center gap-2\">\n          <Button variant=\"outline\" @click=\"decide('rejected')\">Reject analytics</Button>\n          <Button @click=\"decide('accepted')\">Accept</Button>\n          <Button variant=\"ghost\" size=\"sm\">Manage</Button>\n        </div>\n      </div>\n    </div>\n  </Transition>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/cookie-consent/CookieConsentBar.vue"
    },
    {
      "path": "packages/registry-vue/blocks/cookie-consent/CookieConsentPreferences.vue",
      "content": "<script setup lang=\"ts\">\nimport { reactive, ref } from 'vue'\nimport { Lock } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport { Separator } from '@/components/ui/separator'\nimport { Switch } from '@/components/ui/switch'\n\n// Strictly-necessary is listed and shown locked rather than hidden. Omitting it\n// implies there is nothing there, which is not true.\nconst categories = [\n  {\n    id: 'necessary',\n    label: 'Strictly necessary',\n    purpose: 'Session and sign-in state. Without these the app cannot keep you logged in.',\n    retention: 'Session',\n    locked: true,\n  },\n  {\n    id: 'analytics',\n    label: 'Product analytics',\n    purpose: 'Which pages are read and where people give up. Aggregated, never sold.',\n    retention: '13 months',\n    locked: false,\n  },\n  {\n    id: 'support',\n    label: 'Support chat',\n    purpose: 'Keeps a conversation open across page loads so you do not repeat yourself.',\n    retention: '30 days',\n    locked: false,\n  },\n]\n\nconst open = ref(true)\nconst choices = reactive<Record<string, boolean>>({ necessary: true, analytics: false, support: false })\n</script>\n\n<template>\n  <div data-slot=\"cookie-consent-preferences\">\n    <Dialog v-model:open=\"open\">\n      <DialogContent class=\"sm:max-w-lg\">\n        <DialogHeader>\n          <Badge variant=\"secondary\" class=\"w-fit\">Privacy</Badge>\n          <DialogTitle class=\"mt-3 text-xl\">Choose what we store</DialogTitle>\n          <DialogDescription>\n            Each category says what it is for and how long it is kept. Nothing here is advertising.\n          </DialogDescription>\n        </DialogHeader>\n\n        <Separator />\n\n        <ul class=\"space-y-4\">\n          <li v-for=\"category in categories\" :key=\"category.id\" class=\"flex items-start gap-4\">\n            <div class=\"min-w-0 flex-1\">\n              <div class=\"flex flex-wrap items-center gap-2\">\n                <p class=\"text-sm font-medium\">{{ category.label }}</p>\n                <Badge v-if=\"category.locked\" variant=\"outline\" class=\"gap-1\">\n                  <Lock class=\"size-2.5\" aria-hidden=\"true\" />\n                  Required\n                </Badge>\n              </div>\n              <p class=\"text-muted-foreground mt-1 text-xs leading-relaxed\">{{ category.purpose }}</p>\n              <p class=\"text-muted-foreground/80 mt-1 font-mono text-xs\">Kept {{ category.retention }}</p>\n            </div>\n            <Switch\n              v-model=\"choices[category.id]\"\n              :disabled=\"category.locked\"\n              :aria-label=\"`Allow ${category.label}`\"\n              class=\"mt-1 shrink-0\"\n            />\n          </li>\n        </ul>\n\n        <DialogFooter class=\"sm:justify-start\">\n          <Button @click=\"open = false\">Save choices</Button>\n          <Button variant=\"outline\" @click=\"open = false\">Reject optional</Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/cookie-consent/CookieConsentPreferences.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/dialog.json",
    "https://uipkge.dev/r/vue/separator.json",
    "https://uipkge.dev/r/vue/switch.json"
  ],
  "description": "Consent trio covering the whole banner-to-dialog flow: a full-width banner with inline preferences, a bottom bar with equally weighted accept, reject, and manage actions, and a granular preferences dialog with per-category purpose, retention, and switches.",
  "categories": [
    "legal",
    "marketing",
    "feedback"
  ]
}