{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cookie-consent",
  "type": "registry:ui",
  "title": "Cookie consent",
  "description": "A consent bar that morphs into preferences and folds into a reopen button.",
  "categories": [
    "feedback"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/button.json",
    "https://irisui.dev/r/switch.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/cookie-consent/cookie-consent.tsx",
      "content": "\"use client\";\n\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useId,\n  useImperativeHandle,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from \"react\";\nimport { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion } from \"motion/react\";\nimport type { Transition, Variants } from \"motion/react\";\nimport { Cookie, X } from \"lucide-react\";\nimport Button from \"../button/button\";\nimport { Switch } from \"../switch/switch\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\n\n/** One group of cookies a visitor can allow or refuse. */\nexport interface CookieCategory {\n  id: string;\n  label: string;\n  description?: string;\n  /** Needed for the site to work. Always on and cannot be switched off. */\n  required?: boolean;\n  /** Starting position of the switch before a choice is saved. Non-essential categories default to off. */\n  defaultEnabled?: boolean;\n}\n\n/** Category id to allowed. */\nexport type CookieConsentValue = Record<string, boolean>;\nexport type CookieConsentAction = \"accept-all\" | \"reject-all\" | \"save\" | \"reset\";\n\n/** What is written to storage under `storageKey`. */\nexport interface StoredCookieConsent {\n  value: CookieConsentValue;\n  version: string;\n  updatedAt: string;\n}\n\nexport interface CookieConsentHandle {\n  /** Opens the preferences panel, for example from a \"Cookie settings\" footer link. */\n  openPreferences: () => void;\n  /** Closes the preferences panel without saving. */\n  closePreferences: () => void;\n  /** Forgets the saved choice and asks again. */\n  reset: () => void;\n}\n\nexport interface CookieConsentLabels {\n  message: ReactNode;\n  customize: string;\n  acceptAll: string;\n  rejectAll: string;\n  save: string;\n  preferencesTitle: string;\n  preferencesDescription: ReactNode;\n  reopen: string;\n  alwaysOn: string;\n  close: string;\n}\n\n/**\n * A consent bar that morphs into a preferences panel and, once a choice is saved, folds into a small floating button that\n * reopens it. One surface springs between all three shapes. Non-essential categories start off, refusing is as easy as\n * accepting, and the choice is reported through `onChange` and can be remembered in `localStorage`.\n */\nexport interface CookieConsentProps {\n  categories?: CookieCategory[];\n  /** The saved choice. `null` means the visitor has not decided yet. Leave it out to let the component manage it. */\n  value?: CookieConsentValue | null;\n  defaultValue?: CookieConsentValue | null;\n  onChange?: (value: CookieConsentValue | null, detail: { action: CookieConsentAction; updatedAt: Date }) => void;\n  /** Remembers the choice in `localStorage` under this key. Read it elsewhere with `readCookieConsent`. */\n  storageKey?: string;\n  /** Bump when your categories or policy change to ask again. */\n  version?: string;\n  /** Whether the preferences panel is open. */\n  preferencesOpen?: boolean;\n  defaultPreferencesOpen?: boolean;\n  onPreferencesOpenChange?: (open: boolean) => void;\n  /** Link to the full policy, shown in the bar and panel. */\n  policy?: { label: string; href?: string; onClick?: () => void };\n  position?: \"bottom\" | \"bottom-left\" | \"bottom-right\";\n  /** `fixed` pins to the viewport; `absolute` fills the nearest positioned ancestor, useful for previews. */\n  strategy?: \"fixed\" | \"absolute\";\n  /** Keep a small button after a choice so visitors can change their mind. Defaults to true. */\n  showReopenButton?: boolean;\n  /** Corner for the reopen button. Defaults to the bar's side, or left when the bar is centered. */\n  reopenSide?: \"left\" | \"right\";\n  labels?: Partial<CookieConsentLabels>;\n  className?: string;\n}\n\nexport const defaultCookieCategories: CookieCategory[] = [\n  {\n    id: \"necessary\",\n    label: \"Strictly necessary\",\n    description: \"Sign-in, security, and your cart. The site can't work without these.\",\n    required: true,\n  },\n  { id: \"preferences\", label: \"Preferences\", description: \"Remember your language, region, and display settings.\" },\n  { id: \"analytics\", label: \"Analytics\", description: \"Count visits and see which pages help, only in aggregate.\" },\n  { id: \"marketing\", label: \"Marketing\", description: \"Measure campaigns and show relevant ads on other sites.\" },\n];\n\nconst defaultLabels: CookieConsentLabels = {\n  message:\n    \"We use essential cookies to run this site. With your OK, we'd also use optional ones to remember settings and learn what helps.\",\n  customize: \"Customize\",\n  acceptAll: \"Accept all\",\n  rejectAll: \"Reject all\",\n  save: \"Save choices\",\n  preferencesTitle: \"Cookie preferences\",\n  preferencesDescription: \"Choose which optional cookies we can use. You can change this any time.\",\n  reopen: \"Cookie preferences\",\n  alwaysOn: \"Always on\",\n  close: \"Close preferences\",\n};\n\n/** Reads a saved choice, or `null` when there is none or it was saved for another version. Safe to call on the server. */\nexport function readCookieConsent(storageKey: string, version = \"1\"): StoredCookieConsent | null {\n  if (typeof window === \"undefined\") return null;\n  try {\n    const raw = window.localStorage.getItem(storageKey);\n    if (!raw) return null;\n    const parsed = JSON.parse(raw) as Partial<StoredCookieConsent>;\n    if (!parsed || typeof parsed.value !== \"object\" || parsed.value === null || parsed.version !== version) return null;\n    return { value: parsed.value, version: parsed.version, updatedAt: String(parsed.updatedAt ?? \"\") };\n  } catch {\n    return null;\n  }\n}\n\nfunction writeCookieConsent(storageKey: string, stored: StoredCookieConsent | null) {\n  try {\n    if (stored) window.localStorage.setItem(storageKey, JSON.stringify(stored));\n    else window.localStorage.removeItem(storageKey);\n  } catch {\n    /* Storage can be blocked; the callback still reports the choice. */\n  }\n}\n\ntype Mode = \"bar\" | \"panel\" | \"chip\";\ntype Bezier = [number, number, number, number];\nconst enter = [...motionTokens.ease.enter] as Bezier;\nconst standard = [...motionTokens.ease.standard] as Bezier;\n/** Duration springs restated as stiffness and damping, so a retarget mid-flight keeps its velocity. */\nconst physical = (visualDuration: number, bounce: number): Transition => {\n  const root = (2 * Math.PI) / (visualDuration * 1.2);\n  return { type: \"spring\", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };\n};\nconst GROW = physical(0.46, 0),\n  SHRINK = physical(0.38, 0);\nconst BAR_MAX = 660,\n  PANEL_MAX = 420,\n  CHIP = 44,\n  STACK_BELOW = 560;\n\nconst subscribe = () => () => {};\nfunction useHydrated() {\n  return useSyncExternalStore(\n    subscribe,\n    () => true,\n    () => false,\n  );\n}\n\nconst faceVariants: Variants = {\n  hidden: { opacity: 0, y: 10, filter: `blur(${motionTokens.blur.soft}px)` },\n  shown: {\n    opacity: 1,\n    y: 0,\n    filter: \"blur(0px)\",\n    transition: {\n      y: physical(0.42, 0),\n      opacity: { duration: 0.22, ease: enter, delay: 0.04 },\n      filter: { duration: 0.26, ease: enter, delay: 0.04 },\n    },\n  },\n  gone: {\n    opacity: 0,\n    y: 4,\n    filter: `blur(${motionTokens.blur.soft}px)`,\n    transition: { duration: 0.12, ease: standard },\n  },\n};\nconst fadeVariants: Variants = {\n  hidden: { opacity: 0 },\n  shown: { opacity: 1, transition: { duration: 0.16 } },\n  gone: { opacity: 0, transition: { duration: 0.1 } },\n};\n\n/** A face reports its natural height while it is current, then goes inert as it leaves. It is pinned to the side the surface is placed on, so a\n *  centered bar and panel cross-fade in place instead of the incoming face sliding in from the surface's left edge. */\nfunction Face({\n  id,\n  width,\n  side,\n  reduced,\n  focusOnMount,\n  onSize,\n  className,\n  children,\n  ...rest\n}: {\n  id: Mode;\n  width: number;\n  side: \"left\" | \"right\" | \"center\";\n  reduced: boolean;\n  focusOnMount: boolean;\n  onSize: (id: Mode, height: number) => void;\n  className?: string;\n  children: ReactNode;\n  role?: string;\n  \"aria-modal\"?: boolean;\n  \"aria-labelledby\"?: string;\n  \"aria-describedby\"?: string;\n  \"aria-label\"?: string;\n  onKeyDown?: (event: ReactKeyboardEvent<HTMLDivElement>) => void;\n}) {\n  const ref = useRef<HTMLDivElement>(null);\n  const present = useIsPresent();\n  useLayoutEffect(() => {\n    const node = ref.current;\n    if (!node || !present) return;\n    const report = () => onSize(id, node.offsetHeight);\n    report();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(report);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [id, onSize, present]);\n  useEffect(() => {\n    if (!focusOnMount) return;\n    ref.current?.querySelector<HTMLElement>(\"[data-autofocus]\")?.focus({ preventScroll: true });\n  }, [focusOnMount]);\n  return (\n    <motion.div\n      {...rest}\n      ref={ref}\n      className={cn(\n        \"absolute bottom-0 left-0 box-border data-[side=center]:left-1/2 data-[side=center]:-translate-x-1/2 data-[side=right]:right-0 data-[side=right]:left-auto inert:pointer-events-none\",\n        className,\n      )}\n      style={{ width }}\n      data-face={id}\n      data-side={side}\n      variants={reduced ? fadeVariants : faceVariants}\n      initial=\"hidden\"\n      animate=\"shown\"\n      exit=\"gone\"\n      inert={!present || undefined}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport const CookieConsent = forwardRef<CookieConsentHandle, CookieConsentProps>(function CookieConsent(\n  {\n    categories = defaultCookieCategories,\n    value,\n    defaultValue = null,\n    onChange,\n    storageKey,\n    version = \"1\",\n    preferencesOpen,\n    defaultPreferencesOpen = false,\n    onPreferencesOpenChange,\n    policy,\n    position = \"bottom\",\n    strategy = \"fixed\",\n    showReopenButton = true,\n    reopenSide,\n    labels: labelOverrides,\n    className,\n  },\n  ref,\n) {\n  const labels = { ...defaultLabels, ...labelOverrides };\n  const reduced = !!useReducedMotion();\n  const hydrated = useHydrated();\n  const uid = useId();\n  const titleId = `${uid}-title`,\n    descriptionId = `${uid}-description`,\n    messageId = `${uid}-message`;\n\n  /* The saved choice: controlled, or internal and seeded from storage once on the client. */\n  const [internal, setInternal] = useState<CookieConsentValue | null>(defaultValue);\n  const [loaded, setLoaded] = useState(!storageKey || value !== undefined);\n  useEffect(() => {\n    if (loaded || !storageKey) return;\n    const stored = readCookieConsent(storageKey, version);\n    // Storage is only readable after hydration; this runs once to seed the saved choice.\n    if (stored) setInternal(stored.value);\n    setLoaded(true);\n  }, [loaded, storageKey, version]);\n  const consent = value !== undefined ? value : internal;\n  const decided = consent != null;\n\n  const [panelInternal, setPanelInternal] = useState(defaultPreferencesOpen);\n  const panelOpen = preferencesOpen ?? panelInternal;\n  const setPanel = useCallback(\n    (next: boolean) => {\n      if (preferencesOpen === undefined) setPanelInternal(next);\n      onPreferencesOpenChange?.(next);\n    },\n    [onPreferencesOpenChange, preferencesOpen],\n  );\n\n  const baseline = useCallback(\n    (from: CookieConsentValue | null) =>\n      Object.fromEntries(\n        categories.map((category) => [\n          category.id,\n          category.required ? true : (from?.[category.id] ?? category.defaultEnabled ?? false),\n        ]),\n      ),\n    [categories],\n  );\n  const [draft, setDraft] = useState<CookieConsentValue>(() => baseline(consent));\n  const [focusTarget, setFocusTarget] = useState<Mode | null>(null);\n\n  const mode: Mode | null =\n    !hydrated || !loaded ? null : panelOpen ? \"panel\" : !decided ? \"bar\" : showReopenButton ? \"chip\" : null;\n\n  const openPanel = useCallback(\n    (focus = true) => {\n      setDraft(baseline(consent));\n      setFocusTarget(focus ? \"panel\" : null);\n      setPanel(true);\n    },\n    [baseline, consent, setPanel],\n  );\n\n  const closePanel = useCallback(\n    (focus = true) => {\n      setFocusTarget(focus ? (decided ? \"chip\" : \"bar\") : null);\n      setPanel(false);\n    },\n    [decided, setPanel],\n  );\n\n  const commit = useCallback(\n    (next: CookieConsentValue | null, action: CookieConsentAction) => {\n      const updatedAt = new Date();\n      if (value === undefined) setInternal(next);\n      if (storageKey)\n        writeCookieConsent(storageKey, next ? { value: next, version, updatedAt: updatedAt.toISOString() } : null);\n      onChange?.(next, { action, updatedAt });\n    },\n    [onChange, storageKey, value, version],\n  );\n\n  const choose = (action: \"accept-all\" | \"reject-all\" | \"save\") => {\n    const next =\n      action === \"save\"\n        ? { ...draft }\n        : Object.fromEntries(\n            categories.map((category) => [category.id, category.required ? true : action === \"accept-all\"]),\n          );\n    // Keep focus on the page: the choice folds the surface into its reopen button, which takes focus when it exists.\n    setFocusTarget(showReopenButton ? \"chip\" : null);\n    commit(next, action);\n    if (panelOpen) setPanel(false);\n  };\n\n  useImperativeHandle(\n    ref,\n    () => ({\n      openPreferences: () => openPanel(true),\n      closePreferences: () => closePanel(false),\n      reset: () => {\n        setPanel(false);\n        setFocusTarget(null);\n        commit(null, \"reset\");\n      },\n    }),\n    [closePanel, commit, openPanel, setPanel],\n  );\n\n  /* Geometry. The layer is measured; each face gets an explicit width, reports its height, and the surface springs to it. */\n  const layerRef = useRef<HTMLDivElement>(null);\n  const [layerWidth, setLayerWidth] = useState(0);\n  useLayoutEffect(() => {\n    const node = layerRef.current;\n    if (!node) return;\n    const read = () => setLayerWidth(node.clientWidth);\n    read();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(read);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  const align = position === \"bottom-left\" ? \"left\" : position === \"bottom-right\" ? \"right\" : \"center\";\n  const chipSide = reopenSide ?? (align === \"right\" ? \"right\" : \"left\");\n  const widths = { bar: Math.min(layerWidth, BAR_MAX), panel: Math.min(layerWidth, PANEL_MAX), chip: CHIP };\n  const place = (width: number, side: \"left\" | \"right\" | \"center\") =>\n    side === \"left\" ? 0 : side === \"right\" ? layerWidth - width : (layerWidth - width) / 2;\n  // Reopened preferences grow out of the corner button, so the panel opens on its side once a choice exists.\n  const panelSide = decided && showReopenButton ? chipSide : align;\n\n  const x = useMotionValue(0),\n    width = useMotionValue(0),\n    height = useMotionValue(0),\n    radius = useMotionValue(22);\n  const heights = useRef<Record<Mode, number>>({ bar: 0, panel: 0, chip: CHIP });\n  const shape = useRef({ w: 0, h: 0, ready: false });\n  const [tick, setTick] = useState(0);\n  const onSize = useCallback((id: Mode, h: number) => {\n    if (heights.current[id] === h) return;\n    heights.current[id] = h;\n    setTick((count) => count + 1);\n  }, []);\n\n  useLayoutEffect(() => {\n    if (!mode || !layerWidth) {\n      shape.current.ready = false;\n      return;\n    }\n    const w = widths[mode],\n      h = mode === \"chip\" ? CHIP : heights.current[mode];\n    if (!h) return;\n    const next = {\n      x: place(w, mode === \"chip\" ? chipSide : mode === \"panel\" ? panelSide : align),\n      w,\n      h,\n      r: mode === \"chip\" ? CHIP / 2 : mode === \"panel\" ? 26 : 22,\n    };\n    if (!shape.current.ready || reduced) {\n      x.jump(next.x);\n      width.jump(next.w);\n      height.jump(next.h);\n      radius.jump(next.r);\n      shape.current = { w: next.w, h: next.h, ready: true };\n      return;\n    }\n    const transition = next.w * next.h >= shape.current.w * shape.current.h ? GROW : SHRINK;\n    shape.current = { w: next.w, h: next.h, ready: true };\n    animate(x, next.x, transition);\n    animate(width, next.w, transition);\n    animate(height, next.h, transition);\n    animate(radius, next.r, transition);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [mode, layerWidth, tick, reduced, align, chipSide, panelSide]);\n\n  const onPanelKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n    if (event.key !== \"Escape\") return;\n    event.stopPropagation();\n    closePanel(true);\n  };\n\n  const policyLink = policy ? (\n    policy.href ? (\n      <a\n        className=\"cursor-pointer border-0 bg-none p-0 text-ink underline decoration-line-strong underline-offset-3 transition-[text-decoration-color] duration-fast ease-standard hover:decoration-current motion-reduce:transition-none\"\n        href={policy.href}\n        onClick={policy.onClick}\n      >\n        {policy.label}\n      </a>\n    ) : (\n      <button\n        type=\"button\"\n        className=\"cursor-pointer border-0 bg-none p-0 text-ink underline decoration-line-strong underline-offset-3 transition-[text-decoration-color] duration-fast ease-standard hover:decoration-current motion-reduce:transition-none\"\n        onClick={policy.onClick}\n      >\n        {policy.label}\n      </button>\n    )\n  ) : null;\n  const optional = categories.filter((category) => !category.required);\n  const allowedCount = optional.filter((category) => draft[category.id]).length;\n\n  return (\n    <div\n      ref={layerRef}\n      className={cn(\n        \"pointer-events-none fixed right-4 bottom-4 left-4 z-60 h-0 font-body tracking-body\",\n        strategy === \"absolute\" && \"absolute z-5\",\n        className,\n      )}\n      data-position={position}\n    >\n      <AnimatePresence>\n        {mode ? (\n          <motion.div\n            key=\"surface\"\n            className=\"pointer-events-auto absolute bottom-0 left-0 origin-bottom overflow-hidden bg-surface text-ink shadow-[0_0_0_1px_var(--line),var(--shadow-floating)] will-change-[transform,width,height]\"\n            style={{ x, width, height, borderRadius: radius }}\n            initial={reduced ? { opacity: 0 } : { opacity: 0, y: 24, scale: 0.98 }}\n            animate={{\n              opacity: 1,\n              y: 0,\n              scale: 1,\n              transition: reduced\n                ? { duration: 0.16 }\n                : { y: physical(0.5, 0), scale: physical(0.5, 0), opacity: { duration: 0.24, ease: enter } },\n            }}\n            exit={\n              reduced\n                ? { opacity: 0, transition: { duration: 0.12 } }\n                : { opacity: 0, y: 12, scale: 0.96, transition: { duration: 0.18, ease: standard } }\n            }\n          >\n            <AnimatePresence initial={false}>\n              {mode === \"bar\" ? (\n                <Face\n                  key=\"bar\"\n                  id=\"bar\"\n                  width={widths.bar}\n                  side={align}\n                  reduced={reduced}\n                  onSize={onSize}\n                  focusOnMount={focusTarget === \"bar\"}\n                  className=\"@container py-2.5 pr-2.5 pl-[18px]\"\n                  role=\"dialog\"\n                  aria-modal={false}\n                  aria-label={labels.preferencesTitle}\n                  aria-describedby={messageId}\n                >\n                  <div\n                    className=\"group/inner flex items-center gap-4 data-stack:flex-col data-stack:items-stretch data-stack:gap-3 data-stack:pt-1.5 data-stack:pr-2\"\n                    data-stack={widths.bar < STACK_BELOW || undefined}\n                  >\n                    <p\n                      id={messageId}\n                      className=\"m-0 min-w-0 flex-[1_1_auto] text-(length:--text-sm) leading-body text-pretty text-ink-2\"\n                    >\n                      {labels.message}\n                      {policyLink ? <> {policyLink}</> : null}\n                    </p>\n                    <div className=\"flex flex-none items-center gap-1.5 group-data-stack/inner:grid group-data-stack/inner:grid-cols-[auto_1fr_1fr] group-data-stack/inner:@max-[300px]:grid-cols-2 group-data-stack/inner:@max-[300px]:[&>:first-child]:order-1 group-data-stack/inner:@max-[300px]:[&>:first-child]:col-span-full\">\n                      <Button size=\"sm\" variant=\"ghost\" data-autofocus onClick={() => openPanel(true)}>\n                        {labels.customize}\n                      </Button>\n                      <Button size=\"sm\" variant=\"secondary\" onClick={() => choose(\"reject-all\")}>\n                        {labels.rejectAll}\n                      </Button>\n                      <Button size=\"sm\" variant=\"secondary\" onClick={() => choose(\"accept-all\")}>\n                        {labels.acceptAll}\n                      </Button>\n                    </div>\n                  </div>\n                </Face>\n              ) : null}\n\n              {mode === \"panel\" ? (\n                <Face\n                  key=\"panel\"\n                  id=\"panel\"\n                  width={widths.panel}\n                  side={panelSide}\n                  reduced={reduced}\n                  onSize={onSize}\n                  focusOnMount={focusTarget === \"panel\"}\n                  className=\"@container grid gap-3 p-[18px]\"\n                  role=\"dialog\"\n                  aria-modal={false}\n                  aria-labelledby={titleId}\n                  aria-describedby={descriptionId}\n                  onKeyDown={onPanelKeyDown}\n                >\n                  <div className=\"-mt-1 -mb-1.5 flex items-center justify-between gap-3\">\n                    <h2\n                      id={titleId}\n                      className=\"m-0 text-(length:--text-base) leading-body font-medium\"\n                      tabIndex={-1}\n                      data-autofocus\n                    >\n                      {labels.preferencesTitle}\n                    </h2>\n                    <button\n                      type=\"button\"\n                      className=\"-mr-2 inline-grid size-8 flex-none cursor-pointer place-items-center rounded-button border-0 bg-transparent p-0 text-ink-2 [-webkit-tap-highlight-color:transparent] transition-[color,background-color] duration-fast ease-standard hover:bg-hover hover:text-ink active:bg-hover active:text-ink motion-reduce:transition-none\"\n                      aria-label={labels.close}\n                      onClick={() => closePanel(true)}\n                    >\n                      <X size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n                    </button>\n                  </div>\n                  <p id={descriptionId} className=\"m-0 text-(length:--text-sm) leading-body text-pretty text-ink-2\">\n                    {labels.preferencesDescription}\n                    {policyLink ? <> {policyLink}</> : null}\n                  </p>\n                  <ul className=\"-mx-1.5 my-0 grid max-h-[var(--cookie-consent-list-max,min(46vh,300px))] list-none overflow-y-auto overscroll-contain p-0 [scrollbar-width:thin]\">\n                    {categories.map((category) => {\n                      const switchId = `${uid}-${category.id}`,\n                        hintId = `${switchId}-hint`;\n                      return (\n                        <li\n                          key={category.id}\n                          className=\"flex items-center justify-between gap-4 border-t border-line-soft px-1.5 py-2.5 first:border-t-0\"\n                        >\n                          <div className=\"grid min-w-0 gap-0.5\">\n                            <label\n                              htmlFor={category.required ? undefined : switchId}\n                              className=\"text-(length:--text-sm) leading-body font-medium text-ink [&[for]]:cursor-pointer\"\n                            >\n                              {category.label}\n                            </label>\n                            {category.description ? (\n                              <p id={hintId} className=\"m-0 text-(length:--text-xs) leading-body text-ink-3\">\n                                {category.description}\n                              </p>\n                            ) : null}\n                          </div>\n                          {category.required ? (\n                            <span className=\"flex-none text-(length:--text-xs) whitespace-nowrap text-ink-3\">\n                              {labels.alwaysOn}\n                            </span>\n                          ) : (\n                            <Switch\n                              id={switchId}\n                              aria-describedby={category.description ? hintId : undefined}\n                              checked={!!draft[category.id]}\n                              onCheckedChange={(checked) =>\n                                setDraft((current) => ({ ...current, [category.id]: checked }))\n                              }\n                            />\n                          )}\n                        </li>\n                      );\n                    })}\n                  </ul>\n                  <div className=\"grid grid-cols-[1fr_1fr] gap-1.5 border-t border-line-soft pt-2 @min-[340px]:grid-cols-[auto_auto_1fr]\">\n                    <Button size=\"sm\" variant=\"secondary\" onClick={() => choose(\"reject-all\")}>\n                      {labels.rejectAll}\n                    </Button>\n                    <Button size=\"sm\" variant=\"secondary\" onClick={() => choose(\"accept-all\")}>\n                      {labels.acceptAll}\n                    </Button>\n                    <Button\n                      size=\"sm\"\n                      variant=\"primary\"\n                      className=\"col-span-full @min-[340px]:col-auto\"\n                      onClick={() => choose(\"save\")}\n                      aria-describedby={`${uid}-count`}\n                    >\n                      {labels.save}\n                    </Button>\n                    <span\n                      id={`${uid}-count`}\n                      className=\"absolute size-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]\"\n                    >{`${allowedCount} of ${optional.length} optional categories allowed`}</span>\n                  </div>\n                </Face>\n              ) : null}\n\n              {mode === \"chip\" ? (\n                <Face\n                  key=\"chip\"\n                  id=\"chip\"\n                  width={CHIP}\n                  side={chipSide}\n                  reduced={reduced}\n                  onSize={onSize}\n                  focusOnMount={focusTarget === \"chip\"}\n                  className=\"h-11\"\n                >\n                  <button\n                    type=\"button\"\n                    className=\"grid size-11 cursor-pointer place-items-center rounded-button border-0 bg-transparent p-0 text-ink [-webkit-tap-highlight-color:transparent] transition-[background-color] duration-fast ease-standard hover:bg-hover active:bg-hover motion-reduce:transition-none\"\n                    aria-label={labels.reopen}\n                    aria-haspopup=\"dialog\"\n                    data-autofocus\n                    onClick={() => openPanel(true)}\n                  >\n                    <Cookie size={20} strokeWidth={1.75} aria-hidden=\"true\" />\n                  </button>\n                </Face>\n              ) : null}\n            </AnimatePresence>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n});\n\nexport default CookieConsent;\n",
      "type": "registry:component",
      "target": "@components/iris/cookie-consent/cookie-consent.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/cookie-consent",
  "meta": {
    "kind": "component",
    "group": "Feedback",
    "export": "CookieConsent",
    "tags": [
      "feedback",
      "new"
    ],
    "docs": "https://irisui.dev/components/cookie-consent"
  }
}
