{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cookie-consent",
  "title": "Cookie Consent",
  "description": "Cookie consent modal with reusable controller and consent events.",
  "registryDependencies": [
    "@circle-ui/button",
    "@circle-ui/sheet",
    "@circle-ui/switch",
    "@circle-ui/utils"
  ],
  "files": [
    {
      "path": "registry/berlin/blocks/cookie-consent.tsx",
      "content": "// Generated from packages/ui/src/components/cookie-consent.tsx\n\"use client\";\n\nimport * as React from \"react\";\n\nimport { Button } from \"@/registry/berlin/circle-ui/button\";\nimport { Sheet } from \"@/registry/berlin/circle-ui/sheet\";\nimport { Switch } from \"@/registry/berlin/circle-ui/switch\";\nimport { cn } from \"@/registry/berlin/lib/utils\";\n\nexport type CookieConsentScreen = \"main\" | \"settings\";\nexport type CookieConsentChangeSource =\n  | \"accept-all\"\n  | \"reject-all\"\n  | \"save-selection\"\n  | \"hydrate\"\n  | \"reset\";\n\nexport interface CookieConsentCategory {\n  defaultValue?: boolean;\n  description?: string;\n  id: string;\n  label: string;\n  required?: boolean;\n}\n\nexport interface CookieConsentPreferences {\n  consents: Record<string, boolean>;\n  id: string;\n  updatedAt: string;\n  version: string;\n}\n\nexport interface CookieConsentEventDetail {\n  categories: CookieConsentCategory[];\n  consents: Record<string, boolean>;\n  preferences: CookieConsentPreferences;\n  source: CookieConsentChangeSource;\n  version: string;\n}\n\nexport interface CookieConsentPersistPayload extends CookieConsentEventDetail {\n  storageKey: string;\n  text?: string;\n}\n\nexport interface CookieConsentStorageAdapter {\n  getItem: (key: string) => string | null;\n  removeItem: (key: string) => void;\n  setItem: (key: string, value: string) => void;\n}\n\nexport interface CookieConsentControllerOptions {\n  categories?: CookieConsentCategory[];\n  chosenEventName?: string;\n  onPreferencesChange?: (detail: CookieConsentEventDetail) => void;\n  openPreferencesEventName?: string;\n  persist?: (payload: CookieConsentPersistPayload) => void | Promise<void>;\n  persistText?: string;\n  processedDelayMs?: number;\n  processedEventName?: string;\n  storage?: CookieConsentStorageAdapter;\n  storageKey?: string;\n  version?: string;\n}\n\nexport interface CookieConsentController {\n  readonly categories: CookieConsentCategory[];\n  readonly chosenEventName: string;\n  readonly openPreferencesEventName: string;\n  readonly processedDelayMs: number;\n  readonly processedEventName: string;\n  readonly storageKey: string;\n  readonly version: string;\n  delete: () => void;\n  dispatchCurrentPreferences: (\n    source?: Extract<CookieConsentChangeSource, \"hydrate\">,\n  ) => CookieConsentPreferences | null;\n  get: () => CookieConsentPreferences | null;\n  getConsents: () => Record<string, boolean> | null;\n  openPreferences: () => void;\n  reset: () => void;\n  set: (\n    consents: Record<string, boolean>,\n    source: Exclude<CookieConsentChangeSource, \"hydrate\" | \"reset\">,\n  ) => CookieConsentPreferences;\n  subscribe: (\n    listener: (preferences: CookieConsentPreferences | null) => void,\n  ) => () => void;\n  update: (\n    consents: Partial<Record<string, boolean>>,\n    source?: Exclude<CookieConsentChangeSource, \"hydrate\" | \"reset\">,\n  ) => CookieConsentPreferences;\n}\n\nexport interface CookieConsentLabels {\n  acceptAll: React.ReactNode;\n  back: React.ReactNode;\n  customize: React.ReactNode;\n  essential: React.ReactNode;\n  rejectAll: React.ReactNode;\n  required: React.ReactNode;\n  saveSelection: React.ReactNode;\n  settingsAriaLabel: string;\n}\n\nexport interface CookieConsentPolicyLink {\n  href: string;\n  label: React.ReactNode;\n  rel?: string;\n  target?: string;\n}\n\nexport interface CookieConsentProps {\n  autoOpen?: boolean;\n  categories?: CookieConsentCategory[];\n  className?: string;\n  controller?: CookieConsentController;\n  defaultOpen?: boolean;\n  description?: React.ReactNode;\n  dismissible?: boolean;\n  initialScreen?: CookieConsentScreen;\n  labels?: Partial<CookieConsentLabels>;\n  onOpenChange?: (open: boolean) => void;\n  open?: boolean;\n  policyLink?: CookieConsentPolicyLink;\n  title?: React.ReactNode;\n}\n\nconst DEFAULT_STORAGE_KEY = \"ch_cc\";\nconst DEFAULT_VERSION = \"1\";\nconst DEFAULT_OPEN_PREFERENCES_EVENT = \"ch-open-cookie-preferences\";\nconst DEFAULT_CHOSEN_EVENT = \"cookies-chosen\";\nconst DEFAULT_PROCESSED_EVENT = \"cookies-chosen-processed\";\nconst DEFAULT_PROCESSED_DELAY_MS = 1000;\n\nexport const defaultCookieConsentCategories: CookieConsentCategory[] = [\n  {\n    id: \"analytics\",\n    label: \"Analytics\",\n    defaultValue: false,\n  },\n  {\n    id: \"marketing\",\n    label: \"Marketing\",\n    defaultValue: false,\n  },\n  {\n    id: \"personalization\",\n    label: \"Personalization\",\n    required: true,\n    defaultValue: true,\n  },\n];\n\nconst defaultLabels: CookieConsentLabels = {\n  acceptAll: \"Accept all\",\n  back: \"Back\",\n  customize: \"Customize\",\n  essential: \"Essential\",\n  rejectAll: \"Reject all\",\n  required: \"Required\",\n  saveSelection: \"Save selection\",\n  settingsAriaLabel: \"Cookie settings\",\n};\n\nfunction getStorage(\n  storage?: CookieConsentStorageAdapter,\n): CookieConsentStorageAdapter | null {\n  if (storage) return storage;\n  if (typeof window === \"undefined\") return null;\n  return window.localStorage;\n}\n\nfunction createPreferenceId() {\n  if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n    return crypto.randomUUID();\n  }\n\n  return `cc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction normalizeCategories(categories: CookieConsentCategory[]) {\n  return categories.map((category) => ({\n    ...category,\n    defaultValue: category.required ? true : Boolean(category.defaultValue),\n    required: Boolean(category.required),\n  }));\n}\n\nfunction resolveConsents(\n  categories: CookieConsentCategory[],\n  incoming?: Partial<Record<string, boolean>> | null,\n) {\n  return Object.fromEntries(\n    categories.map((category) => [\n      category.id,\n      category.required\n        ? true\n        : incoming?.[category.id] ?? Boolean(category.defaultValue),\n    ]),\n  );\n}\n\nfunction parseStoredPreferences(\n  raw: string | null,\n  categories: CookieConsentCategory[],\n  version: string,\n) {\n  if (!raw) return null;\n\n  try {\n    const parsed = JSON.parse(raw) as Partial<CookieConsentPreferences>;\n    if (!parsed || typeof parsed !== \"object\") return null;\n    if (parsed.version !== version) return null;\n    if (typeof parsed.id !== \"string\" || typeof parsed.updatedAt !== \"string\") {\n      return null;\n    }\n\n    return {\n      id: parsed.id,\n      version,\n      updatedAt: parsed.updatedAt,\n      consents: resolveConsents(categories, parsed.consents ?? {}),\n    } satisfies CookieConsentPreferences;\n  } catch {\n    return null;\n  }\n}\n\nfunction createPreferences(\n  categories: CookieConsentCategory[],\n  consents: Partial<Record<string, boolean>>,\n  version: string,\n  existingId?: string,\n) {\n  return {\n    id: existingId ?? createPreferenceId(),\n    version,\n    updatedAt: new Date().toISOString(),\n    consents: resolveConsents(categories, consents),\n  } satisfies CookieConsentPreferences;\n}\n\nfunction buildEventDetail(\n  categories: CookieConsentCategory[],\n  preferences: CookieConsentPreferences,\n  source: CookieConsentChangeSource,\n) {\n  return {\n    categories,\n    consents: preferences.consents,\n    preferences,\n    source,\n    version: preferences.version,\n  } satisfies CookieConsentEventDetail;\n}\n\nfunction dispatchConsentEvent(\n  eventName: string,\n  detail: CookieConsentEventDetail,\n) {\n  if (typeof document === \"undefined\") return;\n  document.dispatchEvent(new CustomEvent(eventName, { detail }));\n}\n\nfunction createCookieConsentBridgeApi(controller: CookieConsentController) {\n  return {\n    get consents() {\n      return controller.getConsents();\n    },\n    delete() {\n      controller.delete();\n    },\n    dispatchCurrentPreferences() {\n      return controller.dispatchCurrentPreferences(\"hydrate\");\n    },\n    get() {\n      return controller.get();\n    },\n    get openPreferencesEventName() {\n      return controller.openPreferencesEventName;\n    },\n    openPreferences() {\n      controller.openPreferences();\n    },\n    reset() {\n      controller.reset();\n    },\n    set(\n      consents: Record<string, boolean>,\n      source: Exclude<CookieConsentChangeSource, \"hydrate\" | \"reset\"> = \"save-selection\",\n    ) {\n      return controller.set(consents, source);\n    },\n    subscribe(listener: (preferences: CookieConsentPreferences | null) => void) {\n      return controller.subscribe(listener);\n    },\n    update(\n      consents: Partial<Record<string, boolean>>,\n      source: Exclude<CookieConsentChangeSource, \"hydrate\" | \"reset\"> = \"save-selection\",\n    ) {\n      return controller.update(consents, source);\n    },\n  };\n}\n\nexport function createCookieConsentController(\n  options: CookieConsentControllerOptions = {},\n): CookieConsentController {\n  const categories = normalizeCategories(\n    options.categories ?? defaultCookieConsentCategories,\n  );\n  const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;\n  const version = options.version ?? DEFAULT_VERSION;\n  const chosenEventName = options.chosenEventName ?? DEFAULT_CHOSEN_EVENT;\n  const processedEventName =\n    options.processedEventName ?? DEFAULT_PROCESSED_EVENT;\n  const processedDelayMs =\n    options.processedDelayMs ?? DEFAULT_PROCESSED_DELAY_MS;\n  const openPreferencesEventName =\n    options.openPreferencesEventName ?? DEFAULT_OPEN_PREFERENCES_EVENT;\n  const listeners = new Set<\n    (preferences: CookieConsentPreferences | null) => void\n  >();\n  const storage = getStorage(options.storage);\n\n  function read() {\n    if (!storage) return null;\n    return parseStoredPreferences(storage.getItem(storageKey), categories, version);\n  }\n\n  function notify(preferences: CookieConsentPreferences | null) {\n    for (const listener of listeners) listener(preferences);\n  }\n\n  function emit(detail: CookieConsentEventDetail) {\n    dispatchConsentEvent(chosenEventName, detail);\n\n    if (typeof window !== \"undefined\") {\n      window.setTimeout(() => {\n        dispatchConsentEvent(processedEventName, detail);\n      }, processedDelayMs);\n    }\n\n    options.onPreferencesChange?.(detail);\n  }\n\n  function persist(detail: CookieConsentEventDetail) {\n    if (!options.persist) return;\n    void Promise.resolve(\n      options.persist({\n        ...detail,\n        storageKey,\n        text: options.persistText,\n      }),\n    ).catch(() => {\n      console.error(\"Failed to persist cookie consent preferences.\");\n    });\n  }\n\n  function write(\n    consents: Partial<Record<string, boolean>>,\n    source: Exclude<CookieConsentChangeSource, \"hydrate\" | \"reset\">,\n  ) {\n    const existing = read();\n    const next = createPreferences(categories, consents, version, existing?.id);\n\n    storage?.setItem(storageKey, JSON.stringify(next));\n\n    const detail = buildEventDetail(categories, next, source);\n    emit(detail);\n    persist(detail);\n    notify(next);\n\n    return next;\n  }\n\n  return {\n    categories,\n    chosenEventName,\n    delete() {\n      storage?.removeItem(storageKey);\n      notify(null);\n    },\n    dispatchCurrentPreferences(source = \"hydrate\") {\n      const current = read();\n      if (!current) return null;\n\n      const detail = buildEventDetail(categories, current, source);\n      emit(detail);\n      notify(current);\n      return current;\n    },\n    get() {\n      return read();\n    },\n    getConsents() {\n      return read()?.consents ?? null;\n    },\n    openPreferences() {\n      if (typeof window === \"undefined\") return;\n      window.dispatchEvent(new CustomEvent(openPreferencesEventName));\n    },\n    openPreferencesEventName,\n    processedDelayMs,\n    processedEventName,\n    reset() {\n      storage?.removeItem(storageKey);\n      notify(null);\n    },\n    set(consents, source) {\n      return write(consents, source);\n    },\n    storageKey,\n    subscribe(listener) {\n      listeners.add(listener);\n      return () => {\n        listeners.delete(listener);\n      };\n    },\n    update(consents, source = \"save-selection\") {\n      return write({ ...read()?.consents, ...consents }, source);\n    },\n    version,\n  };\n}\n\nexport function attachCookieConsentBridge(\n  target: Window & typeof globalThis = window,\n  controller: CookieConsentController,\n  globalName = \"CH_CC\",\n) {\n  const targetRecord = target as unknown as Record<string, unknown>;\n  const previous = targetRecord[globalName];\n  const bridge = createCookieConsentBridgeApi(controller);\n\n  targetRecord[globalName] = bridge;\n\n  return () => {\n    if (previous === undefined) {\n      delete targetRecord[globalName];\n      return;\n    }\n\n    targetRecord[globalName] = previous;\n  };\n}\n\nfunction useControllableOpenState({\n  defaultOpen = false,\n  onOpenChange,\n  open,\n}: {\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  open?: boolean;\n}) {\n  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n  const isControlled = open !== undefined;\n  const currentOpen = isControlled ? open : internalOpen;\n\n  const setOpen = React.useCallback(\n    (nextOpen: boolean) => {\n      if (!isControlled) setInternalOpen(nextOpen);\n      onOpenChange?.(nextOpen);\n    },\n    [isControlled, onOpenChange],\n  );\n\n  return [currentOpen, setOpen] as const;\n}\n\nfunction useCookieConsentController(\n  controller: CookieConsentController | undefined,\n  categories: CookieConsentCategory[] | undefined,\n) {\n  return React.useMemo(\n    () =>\n      controller ??\n      createCookieConsentController({\n        categories,\n      }),\n    [categories, controller],\n  );\n}\n\nfunction ConsentActionButton({\n  children,\n  kind,\n  onClick,\n}: {\n  children: React.ReactNode;\n  kind: \"outline\" | \"primary\";\n  onClick: React.MouseEventHandler<HTMLButtonElement>;\n}) {\n  return (\n    <Button\n      block\n      className={cn(\n        \"h-12 px-8 py-4 text-[16px] font-medium leading-6 tracking-[-0.16px] normal-case\",\n        kind === \"primary\"\n          ? \"rounded-[56px] border-black bg-black text-white\"\n          : \"rounded-[48px] border-black/10 bg-white text-black focus-visible:border-black/10 focus-visible:bg-white focus-visible:text-black focus-visible:shadow-none\",\n      )}\n      onClick={onClick}\n      theme=\"dark\"\n      variant={kind === \"primary\" ? \"primary\" : \"secondary\"}\n    >\n      {children}\n    </Button>\n  );\n}\n\nexport function CookieConsent({\n  autoOpen = true,\n  categories,\n  className,\n  controller,\n  defaultOpen = false,\n  description,\n  dismissible = false,\n  initialScreen = \"main\",\n  labels: labelsProp,\n  onOpenChange,\n  open,\n  policyLink,\n  title = \"Cookies & similar technologies\",\n}: CookieConsentProps) {\n  const labels = { ...defaultLabels, ...labelsProp };\n  const cookieConsent = useCookieConsentController(controller, categories);\n  const [isOpen, setOpen] = useControllableOpenState({\n    defaultOpen,\n    onOpenChange,\n    open,\n  });\n  const [screen, setScreen] = React.useState<CookieConsentScreen>(initialScreen);\n  const [consents, setConsents] = React.useState<Record<string, boolean>>(\n    () =>\n      cookieConsent.get()?.consents ??\n      resolveConsents(cookieConsent.categories, undefined),\n  );\n  const optionalCategories = React.useMemo(\n    () => cookieConsent.categories.filter((category) => !category.required),\n    [cookieConsent.categories],\n  );\n  const hasRequiredCategories = React.useMemo(\n    () => cookieConsent.categories.some((category) => category.required),\n    [cookieConsent.categories],\n  );\n  const initializedRef = React.useRef(false);\n\n  React.useEffect(() => {\n    setScreen(initialScreen);\n    setConsents(\n      cookieConsent.get()?.consents ??\n        resolveConsents(cookieConsent.categories, undefined),\n    );\n  }, [cookieConsent, initialScreen]);\n\n  React.useEffect(() => {\n    if (initializedRef.current) return;\n    initializedRef.current = true;\n\n    const existing = cookieConsent.get();\n    if (existing) {\n      setConsents(existing.consents);\n      cookieConsent.dispatchCurrentPreferences(\"hydrate\");\n      return;\n    }\n\n    if (autoOpen) setOpen(true);\n  }, [autoOpen, cookieConsent, setOpen]);\n\n  React.useEffect(() => {\n    if (typeof window === \"undefined\") return;\n\n    const openPreferences = () => {\n      setConsents(\n        cookieConsent.get()?.consents ??\n          resolveConsents(cookieConsent.categories, undefined),\n      );\n      setScreen(\"settings\");\n      setOpen(true);\n    };\n\n    window.addEventListener(\n      cookieConsent.openPreferencesEventName,\n      openPreferences as EventListener,\n    );\n\n    return () => {\n      window.removeEventListener(\n        cookieConsent.openPreferencesEventName,\n        openPreferences as EventListener,\n      );\n    };\n  }, [cookieConsent, setOpen]);\n\n  const subtitle =\n    description ??\n    (policyLink ? (\n      <>\n        We use cookies and similar technologies for optional purposes. You can\n        find more details in our{\" \"}\n        <a\n          className=\"text-[#6f7070]\"\n          href={policyLink.href}\n          rel={policyLink.rel}\n          target={policyLink.target}\n        >\n          {policyLink.label}\n        </a>\n        .\n      </>\n    ) : (\n      \"We use cookies and similar technologies for optional purposes.\"\n    ));\n\n  const handleCloseWith = React.useCallback(\n    (\n      nextConsents: Record<string, boolean>,\n      source: Exclude<CookieConsentChangeSource, \"hydrate\" | \"reset\">,\n    ) => {\n      cookieConsent.set(nextConsents, source);\n      setOpen(false);\n      setScreen(initialScreen);\n    },\n    [cookieConsent, initialScreen, setOpen],\n  );\n\n  return (\n    <Sheet\n      className=\"max-w-[375px] md:max-w-[375px]\"\n      dismissible={dismissible}\n      isOpen={isOpen}\n      modal\n      onClose={() => {\n        setOpen(false);\n        setScreen(initialScreen);\n      }}\n    >\n      <div className={cn(\"flex flex-col gap-6\", className)} data-nosnippet>\n        <div className=\"flex flex-col gap-3\">\n          <div className=\"text-[28px] font-medium leading-[150%] tracking-[-0.56px]\">\n            {title}\n          </div>\n          <div className=\"text-[14px] font-normal leading-6 tracking-[-0.14px] text-[#6f7070]\">\n            {subtitle}\n          </div>\n        </div>\n\n        {screen === \"main\" ? (\n          <div className=\"flex flex-col gap-3\">\n            <ConsentActionButton\n              kind=\"primary\"\n              onClick={() =>\n                handleCloseWith(\n                  resolveConsents(\n                    cookieConsent.categories,\n                    Object.fromEntries(\n                      cookieConsent.categories.map((category) => [\n                        category.id,\n                        true,\n                      ]),\n                    ),\n                  ),\n                  \"accept-all\",\n                )\n              }\n            >\n              {labels.acceptAll}\n            </ConsentActionButton>\n            <ConsentActionButton\n              kind=\"primary\"\n              onClick={() =>\n                handleCloseWith(\n                  resolveConsents(\n                    cookieConsent.categories,\n                    Object.fromEntries(\n                      cookieConsent.categories.map((category) => [\n                        category.id,\n                        category.required,\n                      ]),\n                    ),\n                  ),\n                  \"reject-all\",\n                )\n              }\n            >\n              {labels.rejectAll}\n            </ConsentActionButton>\n            <ConsentActionButton\n              kind=\"outline\"\n              onClick={() => setScreen(\"settings\")}\n            >\n              {labels.customize}\n            </ConsentActionButton>\n          </div>\n        ) : (\n          <>\n            <div\n              aria-label={labels.settingsAriaLabel}\n              className=\"flex flex-col gap-4 border-y border-black/10 py-6\"\n            >\n              {hasRequiredCategories ? (\n                <div className=\"flex items-center gap-6\">\n                  <div className=\"flex-1 text-[16px] font-medium leading-6 tracking-[-0.32px] text-black\">\n                    {labels.essential}\n                  </div>\n                  <div className=\"text-right text-[16px] font-normal leading-6 tracking-[-0.16px] whitespace-nowrap text-[#6f7070]\">\n                    {labels.required}\n                  </div>\n                </div>\n              ) : null}\n              {optionalCategories.map((category) => (\n                <div key={category.id} className=\"flex items-center gap-6\">\n                  <div className=\"flex-1 text-[16px] font-medium leading-6 tracking-[-0.32px] text-black\">\n                    {category.label}\n                  </div>\n                  <Switch\n                    ariaLabel={category.label}\n                    checked={Boolean(consents[category.id])}\n                    onChange={(next) =>\n                      setConsents((previous) => ({\n                        ...previous,\n                        [category.id]: next,\n                      }))\n                    }\n                  />\n                </div>\n              ))}\n            </div>\n\n            <div className=\"flex flex-col gap-3\">\n              <ConsentActionButton\n                kind=\"primary\"\n                onClick={() => handleCloseWith(consents, \"save-selection\")}\n              >\n                {labels.saveSelection}\n              </ConsentActionButton>\n              <ConsentActionButton\n                kind=\"outline\"\n                onClick={() => setScreen(\"main\")}\n              >\n                {labels.back}\n              </ConsentActionButton>\n            </div>\n          </>\n        )}\n      </div>\n    </Sheet>\n  );\n}\n",
      "type": "registry:ui",
      "target": "src/components/ui/cookie-consent.tsx"
    }
  ],
  "type": "registry:ui"
}