{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cookie-consent",
  "title": "Cookie Consent",
  "description": "Cookie Consent from the Get IT Done design system.",
  "dependencies": [],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/button.json",
    "https://zero-design.itdone.de/r/button-close.json",
    "https://zero-design.itdone.de/r/heading-tag.json",
    "https://zero-design.itdone.de/r/labels.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/return-focus.json",
    "https://zero-design.itdone.de/r/switch.json",
    "https://zero-design.itdone.de/r/toast-inset.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/cookie-consent.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport { findReplacement, focusPageStart, regionOf } from '@/lib/return-focus';\nimport { useToastInset } from '@/lib/toast-inset';\nimport { cn } from '@/lib/utils';\nimport type { HeadingTag } from '@/components/ui/heading-tag';\nimport { Button } from '@/components/ui/button';\nimport { ButtonClose } from '@/components/ui/button-close';\nimport type { LabelOverrides } from '@/components/ui/labels';\nimport { useLabels } from '@/components/ui/labels-context';\nimport { Switch } from '@/components/ui/switch';\n\nexport interface CookieConsentCategory {\n  id: string;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  // Strictly necessary categories are always on and cannot be switched off. All others start off.\n  necessary?: boolean;\n  // Notice under the description in the settings view, for example a link to the privacy policy. Kept out of the switch description so its link stays a normal link.\n  notice?: React.ReactNode;\n}\n\nexport type CookieConsentSelection = Record<string, boolean>;\n\n// Element of the banner title: a heading level, or `p` for a title that is not part of the document outline.\nexport type CookieConsentTitleTag = HeadingTag | 'p';\n\nexport interface CookieConsentProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {\n  // Visibility is owned by the consumer, who also persists the decision.\n  open?: boolean;\n  title?: React.ReactNode;\n  // Element of the title. h1-h6 fit the banner into the document outline (default h2), `p` renders a plain paragraph so the banner adds no heading to the page.\n  titleAs?: CookieConsentTitleTag;\n  // What is stored and why. Link the privacy policy inside the text.\n  description: React.ReactNode;\n  onAccept: () => void;\n  // Declines everything that is not strictly necessary. Always offered as an equal action next to accept.\n  onDecline: () => void;\n  // Optional third path for per-category choices. Rendered as a quiet link button.\n  onCustomize?: () => void;\n  // Per-category choices. Adds a settings view with one switch per category (nothing pre-selected except necessary ones).\n  categories?: CookieConsentCategory[];\n  // Stored choice that the settings view starts from, for example the cookie value of a returning visitor. Uncontrolled: the switches stay editable and a changed `initialConsent` resets them. Unknown ids are ignored, missing ids stay off, necessary categories are always on. Without it nothing is pre-selected.\n  initialConsent?: CookieConsentSelection;\n  // Controlled selection of the settings view. Wins over `initialConsent`; pair it with `onValueChange`. Without `value` the component keeps the switch state itself.\n  value?: CookieConsentSelection;\n  // Called on every switch change in the settings view with the full selection (both modes).\n  onValueChange?: (selection: CookieConsentSelection) => void;\n  // Called when `openCookieSettings()` or `useCookieConsent().reopen()` asks for the banner. A controlled consumer sets its `open` state to true here. The banner shows itself for the request even without this handler, and hides again after a decision.\n  onReopen?: () => void;\n  // Receives the chosen categories from the settings view.\n  onSave?: (selection: CookieConsentSelection) => void;\n  // Shows a close button. By default closing gives no consent: it calls `onDecline`, exactly like declining (see `dismissBehavior`).\n  closable?: boolean;\n  // What the close button does. `decline` (default) calls `onDecline`. `defer` closes without deciding: nothing is decided or stored, `onDismiss` is called, the banner hides itself and shows again on the next mount (next visit).\n  dismissBehavior?: 'decline' | 'defer';\n  // Called when the banner is closed with `dismissBehavior=\"defer\"`.\n  onDismiss?: () => void;\n  // `compact` is the bottom-left card, `centered-wide` is a wide centred bar/card.\n  layout?: 'compact' | 'centered-wide';\n  // Opt-in brand fill for both decisions. Accept and decline always render with identical variant, size and contrast\n  // on the first layer, because declining must be as easy and as prominent as accepting (GDPR / TTDSG).\n  acceptEmphasis?: boolean;\n  acceptLabel?: string;\n  declineLabel?: string;\n  customizeLabel?: string;\n  closeLabel?: string;\n  saveLabel?: string;\n  backLabel?: string;\n  // Text beside the locked switch of necessary categories in the settings view. Default \"Always active\" (en) or \"Immer aktiv\" (de).\n  alwaysActiveLabel?: string;\n  settingsTitle?: React.ReactNode;\n  // Accessible name of the region.\n  regionLabel?: string;\n  // `fixed` pins the banner to the bottom of the window, `static` leaves placement to the layout.\n  positioning?: 'fixed' | 'static';\n  // Size of the buttons: `default` (40px) or `sm` (32px, for dense pages and the compact card).\n  size?: 'default' | 'sm';\n}\n\n// Breathing room in px between a scrolled-to element and the banner edge, so its focus ring stays visible.\nconst SCROLL_PADDING_GAP = 8;\n\n// Links inside the running text keep an underline: their colour alone is not 3:1 against the surrounding text (WCAG 1.4.1).\nconst inlineLink = '[&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:no-underline';\n\n// Elements that only take programmatic focus (the region and its title, tabIndex -1) are not controls, so they do not get the full focus ring of the library. Pointer users see nothing; keyboard users (:focus-visible) get a thin outline.\nconst programmaticFocusRing = 'outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring-default';\n\n// A trigger is a button or link outside the banner. Only such an element holding focus proves that the user asked for the banner.\nconst isTrigger = (el: Element | null, banner: HTMLElement | null): el is HTMLElement =>\n  el instanceof HTMLElement && !banner?.contains(el) && el.matches('button, a[href], [role=\"button\"], [role=\"link\"]');\n\nconst scrollsVertically = (el: HTMLElement) => {\n  const { overflowY } = window.getComputedStyle(el);\n  return /(auto|scroll|overlay)/.test(overflowY) && el.scrollHeight > el.clientHeight;\n};\n\n// Brings a focused element clear of the banner. scrollIntoView covers every ancestor scroller (also horizontal carousels, where the page scroll padding does not apply), the correction afterwards covers scrollers that ignore the document scroll padding.\nconst revealAboveBanner = (target: Element, banner: HTMLElement) => {\n  target.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });\n  const bannerRect = banner.getBoundingClientRect();\n  const rect = target.getBoundingClientRect();\n  const horizontalOverlap = rect.right > bannerRect.left && rect.left < bannerRect.right;\n  const hiddenBy = rect.bottom - (bannerRect.top - SCROLL_PADDING_GAP);\n  if (!horizontalOverlap || hiddenBy <= 0 || rect.top >= bannerRect.bottom) return;\n  const delta = Math.min(hiddenBy, Math.max(0, rect.top));\n  if (delta <= 0) return;\n  let ancestor = target.parentElement;\n  while (ancestor && ancestor !== document.body && !scrollsVertically(ancestor)) ancestor = ancestor.parentElement;\n  if (ancestor && ancestor !== document.body) ancestor.scrollTop += delta;\n  else window.scrollBy(0, delta);\n};\n\nconst REOPEN_EVENT = 'kette:cookie-consent-reopen';\n\ninterface ReopenRequest {\n  opener: HTMLElement | null;\n  handled: boolean;\n}\n\n// Opens the consent banner again, for example from a \"Cookie settings\" link in the footer. Call it from the click handler of the opener: the focused button or link is remembered, focus moves into the banner and returns to the opener when the banner closes. Works with a controlled banner (`open`, pair it with `onReopen`) and an uncontrolled one. Returns false when no CookieConsent is mounted, so keep the component mounted (with `open={false}` while a choice is stored).\nconst openCookieSettings = (): boolean => {\n  if (typeof window === 'undefined') return false;\n  const active = document.activeElement;\n  const detail: ReopenRequest = { opener: active instanceof HTMLElement && active !== document.body ? active : null, handled: false };\n  window.dispatchEvent(new CustomEvent<ReopenRequest>(REOPEN_EVENT, { detail }));\n  return detail.handled;\n};\n\n// Hook form of `openCookieSettings`, with a stable `reopen` function.\nconst useCookieConsent = () => React.useMemo(() => ({ reopen: openCookieSettings }), []);\n\nconst initialSelection = (categories: CookieConsentCategory[], stored?: CookieConsentSelection): CookieConsentSelection =>\n  Object.fromEntries(categories.map((c) => [c.id, Boolean(c.necessary) || Boolean(stored?.[c.id])]));\n\n// Privacy-preserving by design: nothing is pre-selected, accept and decline share variant, size and order weight,\n// and the banner does not trap focus or block the page.\nconst CookieConsent = React.forwardRef<HTMLElement, CookieConsentProps>(\n  (\n    {\n      className,\n      open = true,\n      title,\n      titleAs = 'h2',\n      description,\n      onAccept,\n      onDecline,\n      onCustomize,\n      categories,\n      initialConsent,\n      value,\n      onValueChange,\n      onSave,\n      onReopen,\n      closable = false,\n      dismissBehavior = 'decline',\n      onDismiss,\n      layout = 'compact',\n      acceptEmphasis = false,\n      acceptLabel,\n      declineLabel,\n      customizeLabel,\n      closeLabel,\n      saveLabel,\n      backLabel,\n      alwaysActiveLabel,\n      settingsTitle,\n      regionLabel,\n      positioning = 'fixed',\n      size = 'default',\n      onKeyDown,\n      ...props\n    },\n    ref,\n  ) => {\n    const labels: LabelOverrides = {\n      acceptAllCookies: acceptLabel,\n      declineNonEssentialCookies: declineLabel,\n      cookieSettings: customizeLabel,\n      cookieClose: closeLabel,\n      cookieSaveSelection: saveLabel,\n      cookieBack: backLabel,\n      cookieAlwaysActive: alwaysActiveLabel,\n      cookieSettingsTitle: typeof settingsTitle === 'string' ? settingsTitle : undefined,\n      cookieConsentRegion: regionLabel,\n    };\n    const l = useLabels(labels);\n    const Title = titleAs as React.ElementType;\n    const titleId = React.useId();\n    const [view, setView] = React.useState<'summary' | 'settings'>('summary');\n    const [draft, setDraft] = React.useState<CookieConsentSelection>(() => initialSelection(categories ?? [], value ?? initialConsent));\n    const controlled = value !== undefined;\n    const selection = controlled ? initialSelection(categories ?? [], value) : draft;\n    // A new stored choice (for example after saving) resets the uncontrolled switches.\n    const storedKey = JSON.stringify(initialConsent ?? null);\n    const firstStored = React.useRef(true);\n    React.useEffect(() => {\n      if (firstStored.current) {\n        firstStored.current = false;\n        return;\n      }\n      setDraft(initialSelection(categories ?? [], initialConsent));\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [storedKey]);\n    const [deferred, setDeferred] = React.useState(false);\n    // Below sm the description is clamped to two lines (the card stays small and covers little of the page); a toggle shows the rest. It only appears when the text really is longer.\n    const [expanded, setExpanded] = React.useState(false);\n    const [clamped, setClamped] = React.useState(false);\n    const descriptionRef = React.useRef<HTMLDivElement | null>(null);\n    const detailsId = `${titleId}-details`;\n    const headingRef = React.useRef<HTMLElement | null>(null);\n    const sectionRef = React.useRef<HTMLElement | null>(null);\n    const settingsButtonRef = React.useRef<HTMLButtonElement | null>(null);\n    // The banner never takes focus on its own. Only a user-driven switch between the views moves it: into the settings panel on open, back to the Settings button on return.\n    const viewChanged = React.useRef(false);\n    React.useEffect(() => {\n      if (!viewChanged.current) return;\n      if (view === 'settings') (headingRef.current ?? sectionRef.current)?.focus({ preventScroll: true });\n      else settingsButtonRef.current?.focus({ preventScroll: true });\n    }, [view]);\n    // A reopen request shows the banner even when the consumer's `open` is false. A decision or `open` turning false takes it back.\n    const [requested, setRequested] = React.useState(false);\n    const visible = (open || requested) && !deferred;\n    const visibleRef = React.useRef(visible);\n    visibleRef.current = visible;\n    const onReopenRef = React.useRef(onReopen);\n    onReopenRef.current = onReopen;\n    const requestedOpener = React.useRef<HTMLElement | null>(null);\n    const opener = React.useRef<HTMLElement | null>(null);\n    const openerScope = React.useRef<HTMLElement | null>(null);\n    const rememberOpener = (el: HTMLElement | null) => {\n      opener.current = el;\n      openerScope.current = regionOf(el);\n    };\n    // True while keyboard focus sits inside the banner. Only then does closing it owe the user a new place for focus.\n    const focusInside = React.useRef(false);\n    // Restores focus to the opener. The replacement is searched in the opener's region only, and the page never scrolls: nobody asked for the move.\n    // Without an opener (first visit, the banner appeared on its own) focus that was inside the banner goes to the start of the page content instead of being lost to the body (Escape, accept, decline).\n    const restoreFocus = () => {\n      const target = findReplacement(opener.current, openerScope.current);\n      opener.current = null;\n      openerScope.current = null;\n      const active = document.activeElement;\n      const lost = !active || active === document.body || !active.isConnected;\n      if (target && lost) target.focus({ preventScroll: true });\n      else if (!target && lost && focusInside.current) focusPageStart();\n      focusInside.current = false;\n    };\n    const wasVisible = React.useRef(false);\n    const wasOpen = React.useRef(open);\n    React.useEffect(() => {\n      if (wasOpen.current && !open) setRequested(false);\n      wasOpen.current = open;\n    }, [open]);\n    React.useEffect(() => {\n      const onRequest = (event: Event) => {\n        const detail = (event as CustomEvent<ReopenRequest>).detail;\n        if (detail) detail.handled = true;\n        requestedOpener.current = detail?.opener ?? null;\n        setDeferred(false);\n        setRequested(true);\n        onReopenRef.current?.();\n        // Already showing: no visibility change follows, so the focus move happens here.\n        if (visibleRef.current && isTrigger(requestedOpener.current, sectionRef.current)) {\n          rememberOpener(requestedOpener.current);\n          requestedOpener.current = null;\n          viewChanged.current = false;\n          setView('summary');\n          (headingRef.current ?? sectionRef.current)?.focus({ preventScroll: true });\n        }\n      };\n      window.addEventListener(REOPEN_EVENT, onRequest);\n      return () => window.removeEventListener(REOPEN_EVENT, onRequest);\n    }, []);\n    // A reopen (`open` turns true again) shows a banner that was dismissed before, so a deferred dismissal no longer hides it.\n    React.useEffect(() => {\n      if (open) setDeferred(false);\n    }, [open]);\n    // The very first automatic appearance never moves focus. When the banner appears while a button or link outside it holds focus (a \"Cookie settings\" trigger in the footer), focus moves to the heading, or to the region without a title, and the trigger is remembered. When the banner goes away (accept, decline, save, close) focus returns to that trigger, also if it was re-rendered meanwhile.\n    React.useEffect(() => {\n      if (visible) {\n        const fromRequest = requestedOpener.current;\n        requestedOpener.current = null;\n        const active = fromRequest?.isConnected ? fromRequest : document.activeElement;\n        if (isTrigger(active, sectionRef.current)) {\n          rememberOpener(active);\n          viewChanged.current = false;\n          setView('summary');\n          (headingRef.current ?? sectionRef.current)?.focus({ preventScroll: true });\n        } else {\n          rememberOpener(null);\n        }\n      } else if (wasVisible.current) {\n        restoreFocus();\n      }\n      wasVisible.current = visible;\n    }, [visible]);\n    // A banner that a consumer removes from the tree (conditional rendering) also hands focus back.\n    React.useEffect(\n      () => () => restoreFocus(),\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n      [],\n    );\n    // Measures whether the clamped description hides text. Runs while collapsed; an expanded description keeps its toggle.\n    React.useEffect(() => {\n      const node = descriptionRef.current;\n      if (!node || expanded || typeof ResizeObserver === 'undefined') return undefined;\n      const measure = () => setClamped(node.scrollHeight > node.clientHeight + 1);\n      measure();\n      const observer = new ResizeObserver(measure);\n      observer.observe(node);\n      return () => observer.disconnect();\n    }, [expanded, visible, view, description]);\n    const reserveScrollSpace = visible && positioning === 'fixed';\n    // On phones a toast rides above the fixed banner instead of covering its buttons.\n    useToastInset(sectionRef, { enabled: reserveScrollSpace });\n    // While a fixed banner is showing, keep focused page content above it. Its height is reserved as scroll padding on the document scroller, and as an end-of-body spacer so the end of the page can scroll clear of it too, whatever the body height.\n    React.useEffect(() => {\n      const node = sectionRef.current;\n      if (!reserveScrollSpace || !node || typeof ResizeObserver === 'undefined') return undefined;\n      const root = document.documentElement;\n      const previous = root.style.scrollPaddingBottom;\n      const spacer = document.createElement('div');\n      spacer.setAttribute('aria-hidden', 'true');\n      spacer.setAttribute('data-cookie-consent-spacer', '');\n      document.body.appendChild(spacer);\n      const update = () => {\n        const rect = node.getBoundingClientRect();\n        const covered = Math.ceil(rect.height + Math.max(0, root.clientHeight - rect.bottom)) + SCROLL_PADDING_GAP;\n        root.style.scrollPaddingBottom = `${covered}px`;\n        spacer.style.height = `${covered}px`;\n      };\n      update();\n      const observer = new ResizeObserver(update);\n      observer.observe(node);\n      return () => {\n        observer.disconnect();\n        root.style.scrollPaddingBottom = previous;\n        spacer.remove();\n      };\n    }, [reserveScrollSpace]);\n    // Keyboard focus moving into page content while the banner is open must never end up behind it, in horizontal scrollers too.\n    React.useEffect(() => {\n      const node = sectionRef.current;\n      if (!reserveScrollSpace || !node) return undefined;\n      const onFocusIn = (event: FocusEvent) => {\n        const target = event.target;\n        if (!(target instanceof Element) || node.contains(target) || target === document.body) return;\n        revealAboveBanner(target, node);\n      };\n      document.addEventListener('focusin', onFocusIn);\n      return () => document.removeEventListener('focusin', onFocusIn);\n    }, [reserveScrollSpace]);\n    if (!visible) return null;\n    const hasSettings = Boolean(categories?.length);\n    const inSettings = hasSettings && view === 'settings';\n    const wide = layout === 'centered-wide';\n    // Both decisions share one variant so neither can be made more prominent than the other.\n    const decisionVariant = acceptEmphasis ? 'primary' : 'outline';\n    const heading = inSettings ? (settingsTitle ?? l.cookieSettingsTitle) : title;\n    // Every decision also ends a reopen request.\n    const decline = () => {\n      setRequested(false);\n      onDecline();\n    };\n    const accept = () => {\n      setRequested(false);\n      onAccept();\n    };\n    const close = () => {\n      if (dismissBehavior === 'defer') {\n        setDeferred(true);\n        setRequested(false);\n        onDismiss?.();\n      } else {\n        decline();\n      }\n    };\n    const openSettings = () => {\n      onCustomize?.();\n      if (hasSettings) {\n        if (!controlled && initialConsent) setDraft(initialSelection(categories ?? [], initialConsent));\n        viewChanged.current = true;\n        setView('settings');\n      }\n    };\n    const closeSettings = () => {\n      viewChanged.current = true;\n      setView('summary');\n    };\n    // Escape closes the banner exactly like the close button, so it only does something when `closable` is set (decline by default, or the deferral with `dismissBehavior=\"defer\"`). Without a close action the banner stays: Escape never grants or declines consent on its own. In the settings view Escape goes back to the summary first.\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n      onKeyDown?.(event);\n      if (event.defaultPrevented || event.key !== 'Escape') return;\n      if (inSettings) {\n        event.preventDefault();\n        closeSettings();\n      } else if (closable) {\n        event.preventDefault();\n        close();\n      }\n    };\n    const buttonSize = size === 'sm' ? 'sm' : 'default';\n    return (\n      <section\n        onKeyDown={handleKeyDown}\n        onFocus={() => {\n          focusInside.current = true;\n        }}\n        onKeyDownCapture={() => {\n          focusInside.current = true;\n        }}\n        onClickCapture={() => {\n          focusInside.current = true;\n        }}\n        onBlur={(event) => {\n          // Moving between controls of the banner keeps it. Focus that left for the page ends it. A control that was removed while it had focus (the banner closing) is not a move.\n          const next = event.relatedTarget as Node | null;\n          if (!event.currentTarget.contains(next) && (next || (event.target as Element).isConnected)) focusInside.current = false;\n        }}\n        data-size={size}\n        ref={(node) => {\n          sectionRef.current = node;\n          if (typeof ref === 'function') ref(node);\n          else if (ref) (ref as React.MutableRefObject<HTMLElement | null>).current = node;\n        }}\n        tabIndex={-1}\n        aria-label={heading ? undefined : l.cookieConsentRegion}\n        aria-labelledby={heading ? titleId : undefined}\n        className={cn(\n          'relative z-banner flex flex-col gap-4 rounded-surface border border-solid border-border bg-popover p-6 text-popover-foreground shadow-elevation-raised max-sm:gap-3 max-sm:p-4',\n          programmaticFocusRing,\n          wide ? 'max-w-4xl' : 'max-w-reading sm:flex-row sm:items-end',\n          wide && !inSettings && 'md:flex-row md:items-end',\n          positioning === 'fixed' && (wide ? 'fixed inset-x-4 bottom-4 mx-auto' : 'fixed bottom-4 left-4 right-4 sm:right-auto sm:w-[calc(100%-2rem)]'),\n          positioning === 'static' && 'w-full',\n          positioning === 'static' && wide && 'mx-auto',\n          className,\n        )}\n        {...props}\n      >\n        {closable ? (\n          <ButtonClose aria-label={l.cookieClose} className=\"absolute right-3 top-3\" onClick={close} />\n        ) : null}\n        {/* Below sm the text area has a ceiling and scrolls inside the card, so a long description or a list of categories never pushes the buttons off a short screen. */}\n        <div className={cn('flex min-w-0 flex-1 flex-col gap-2 max-sm:max-h-[min(50dvh,20rem)] max-sm:overflow-y-auto', closable && 'pr-8')}>\n          {heading ? (\n            <Title ref={headingRef} id={titleId} tabIndex={-1} className={cn('typography-ui-semibold m-0', programmaticFocusRing)}>\n              {heading}\n            </Title>\n          ) : null}\n          {inSettings ? (\n            <ul className=\"m-0 flex list-none flex-col gap-4 p-0\">\n              {categories?.map((category) => {\n                const nameId = `${titleId}-${category.id}-name`;\n                const descId = `${titleId}-${category.id}-desc`;\n                return (\n                  <li key={category.id} className=\"flex items-start justify-between gap-4\">\n                    <div className=\"flex min-w-0 flex-col gap-1\">\n                      <span id={nameId} className=\"typography-ui-semibold\">\n                        {category.title}\n                      </span>\n                      {category.description ? (\n                        <span id={descId} className=\"typography-ui text-text-subtle\">\n                          {category.description}\n                        </span>\n                      ) : null}\n                      {category.notice ? (\n                        <span data-slot=\"notice\" className={cn('typography-ui text-text-subtle', inlineLink)}>\n                          {category.notice}\n                        </span>\n                      ) : null}\n                    </div>\n                    <div className=\"flex shrink-0 items-center gap-2\">\n                      {category.necessary ? <span className=\"typography-ui text-text-subtle\">{l.cookieAlwaysActive}</span> : null}\n                      <Switch\n                        checked={category.necessary ? true : Boolean(selection[category.id])}\n                        disabled={category.necessary}\n                        onCheckedChange={(checked) => {\n                          const next = { ...selection, [category.id]: checked };\n                          if (!controlled) setDraft(next);\n                          onValueChange?.(next);\n                        }}\n                        aria-labelledby={nameId}\n                        aria-describedby={category.description ? descId : undefined}\n                      />\n                    </div>\n                  </li>\n                );\n              })}\n            </ul>\n          ) : (\n            <div\n                ref={descriptionRef}\n                id={detailsId}\n                // A link inside the clamped text that takes keyboard focus opens the text, so it is never focused out of sight.\n                onFocus={() => setExpanded(true)}\n                className={cn('typography-ui text-text-subtle', inlineLink, !expanded && 'max-sm:line-clamp-2')}\n              >\n                {description}\n              </div>\n          )}\n        </div>\n        {inSettings ? (\n          <div className=\"grid shrink-0 grid-cols-2 items-stretch gap-3 sm:flex sm:flex-row sm:flex-wrap sm:items-center\">\n            <Button type=\"button\" variant=\"outline\" size={buttonSize} onClick={closeSettings}>\n              {l.cookieBack}\n            </Button>\n            <Button type=\"button\" variant=\"outline\" size={buttonSize} onClick={() => {\n                setRequested(false);\n                onSave?.({ ...selection });\n              }}>\n              {l.cookieSaveSelection}\n            </Button>\n          </div>\n        ) : (\n          // Below sm the two decisions stay full width (a label never wraps) and the settings link and the Show more toggle share the row below them, as links of 32px: the card was 370px tall at 320x568 with three full stacked buttons and no clamp.\n          <div className=\"grid shrink-0 grid-cols-2 items-stretch gap-3 sm:flex sm:flex-row sm:flex-wrap sm:items-center\">\n            <Button type=\"button\" variant={decisionVariant} size={buttonSize} className=\"max-sm:col-span-2\" onClick={decline}>\n              {l.declineNonEssentialCookies}\n            </Button>\n            <Button type=\"button\" variant={decisionVariant} size={buttonSize} className=\"max-sm:col-span-2\" onClick={accept}>\n              {l.acceptAllCookies}\n            </Button>\n            {onCustomize || hasSettings ? (\n              <Button ref={settingsButtonRef} type=\"button\" variant=\"link\" size={buttonSize} className=\"max-sm:col-start-1 max-sm:justify-self-start\" onClick={openSettings}>\n                {l.cookieSettings}\n              </Button>\n            ) : null}\n            {clamped || expanded ? (\n              <Button\n                type=\"button\"\n                variant=\"link\"\n                size={buttonSize}\n                aria-expanded={expanded}\n                aria-controls={detailsId}\n                className=\"max-sm:col-start-2 max-sm:justify-self-end sm:hidden\"\n                onClick={() => setExpanded((v) => !v)}\n              >\n                {expanded ? l.cookieShowLess : l.cookieShowMore}\n              </Button>\n            ) : null}\n          </div>\n        )}\n      </section>\n    );\n  },\n);\nCookieConsent.displayName = 'CookieConsent';\n\nexport { CookieConsent, openCookieSettings, useCookieConsent };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}