{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "accordion",
  "type": "registry:ui",
  "title": "Accordion",
  "description": "Fluent 2-styled accordion (expand/collapse groups) with a shadcn API. Built on @base-ui/react's Accordion for open/close state and a height-animated collapsible panel; the trigger rotates a ChevronDownRegular chevron and the panel animates height via Base UI's --accordion-panel-height CSS variable.",
  "author": "graundtech <https://github.com/graundtech/fluent2-react-kit>",
  "dependencies": [
    "@base-ui/react",
    "@fluentui/react-icons"
  ],
  "registryDependencies": [
    "https://fluent2-react-kit.graund.io/r/utils.json"
  ],
  "files": [
    {
      "path": "components/ui/accordion.tsx",
      "type": "registry:ui",
      "target": "components/ui/accordion.tsx",
      "content": "\"use client\";\n\nimport { Accordion as AccordionPrimitive } from \"@base-ui/react/accordion\";\nimport { ChevronDownRegular } from \"@fluentui/react-icons\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Accordion — Fluent 2-styled, shadcn-API accordion (expand/collapse groups).\n *\n * ## Base UI mapping (conventions §9)\n * Behavior — open/close state, focus-safe disabled triggers, and a\n * height-animated collapsible panel — genuinely needs a primitive, so the\n * parts wrap `@base-ui/react/accordion` (namespace import, matching the\n * actual `export * as Accordion from \"./index.parts.js\"` shape in\n * node_modules, same pattern as `select.tsx`/`checkbox.tsx`). shadcn part\n * names map onto Base UI's model:\n *\n * | Exported (shadcn name) | Base UI primitive        |\n * | ----------------------- | ------------------------ |\n * | `Accordion`              | `Accordion.Root`         |\n * | `AccordionItem`          | `Accordion.Item`         |\n * | `AccordionTrigger`       | `Accordion.Header` (h3) + `Accordion.Trigger` (button), composed like shadcn |\n * | `AccordionContent`       | `Accordion.Panel`        |\n *\n * ## Divergences from the shadcn/Radix Accordion API (all deliberate)\n * 1. **No `type=\"single\" | \"multiple\"` prop.** Radix's Accordion takes a\n *    `type` discriminant that also changes the shape of `value`\n *    (`string | undefined` for `\"single\"`, `string[]` for `\"multiple\"`).\n *    Base UI instead has a single `multiple?: boolean` prop (default\n *    `false`) and *always* represents the open set as an array\n *    (`AccordionValue<Value> = Value[]`), even in single mode (an array of\n *    zero or one items). This wrapper does not paper over that with a fake\n *    `type` shim — `<Accordion>` passes `AccordionPrimitive.Root.Props`\n *    straight through, so callers use `multiple` and read/write `value`/\n *    `defaultValue`/`onValueChange` as arrays. `multiple=false` (the\n *    default) still enforces \"opening one closes the others\" the same way\n *    Radix's `type=\"single\"` does — Base UI's root closes any previously\n *    open item when a new one opens and `multiple` is false.\n * 2. **`AccordionItem` takes `value` directly** (not wrapped/renamed) — same\n *    prop name as Base UI, and it auto-generates a stable id if omitted, so\n *    `value` is optional here (unlike Radix, where an uncontrolled\n *    `type=\"single\"` item still needs an explicit `value` to open by\n *    default).\n *\n * ## Chevron rotation\n * `AccordionTrigger` carries Base UI's own `data-panel-open` presence\n * attribute (from `AccordionTriggerDataAttributes.panelOpen`, verified\n * against the compiled source — *not* `data-open`, which lives on\n * `Accordion.Item`/`Accordion.Panel` instead) when its panel is open. The\n * chevron is a child of the trigger button, so the trigger carries `group`\n * and the chevron targets `group-data-[panel-open]:rotate-180` (conventions\n * §4's bracketed data-attribute form) — the bracket form works on ancestor\n * *and* self, `group-` just relays the parent's attribute to a descendant\n * selector.\n *\n * ## Height animation\n * `Accordion.Panel` measures its own content and exposes it as two CSS\n * custom properties set as inline styles on the panel element itself:\n * `--accordion-panel-height` and `--accordion-panel-width` (verified against\n * `AccordionPanelCssVars` in the compiled source — during open/close these\n * update live as Base UI's `ResizeObserver` re-measures). `AccordionContent`\n * reads `--accordion-panel-height` back via `h-[var(--accordion-panel-height)]`\n * on the outer, `overflow-hidden` panel and transitions `height` (never\n * `transition-transform`/`scale`/`translate` — height is the property that\n * actually changes here, conventions §3.5's rule generalized) with token\n * durations/easings: `duration-normal ease-decelerate-mid` entering,\n * `ease-accelerate-mid` (via `data-ending-style:ease-accelerate-mid`, which\n * wins by source order) exiting. `data-starting-style`/`data-ending-style`\n * (Base UI's enter/exit hooks, already used by `select.tsx`'s popup) pin the\n * height to `0` at both animation boundaries, so the transition always\n * animates *from* `0` *to* the measured height (open) or the reverse\n * (close) — this is Base UI's own documented pattern for this component,\n * reproduced with the kit's tokens instead of raw CSS. Padding lives on an\n * *inner* `div` (not the animated panel itself) so the padding doesn't\n * distort the measured/animated height — the same inner-wrapper split\n * shadcn's own Radix-based `AccordionContent` uses. `AccordionPanel`\n * unmounts while closed by default (`keepMounted` is not forced on); Base\n * UI's own transition-status tracking (mirrors Radix `Presence`) keeps it\n * mounted for the *closing* animation without any extra prop here.\n *\n * ## `\"use client\"` — required (conventions §9)\n * Every Base UI Accordion part module carries its own `'use client'`\n * directive, so on that basis alone this wrapper could stay\n * server-renderable (same reasoning as `avatar.tsx`). But\n * `ChevronDownRegular` from `@fluentui/react-icons` breaks that: the\n * package's shared icon-sizing module (`createFluentIcon.styles.js`) calls\n * `@griffel/react`'s `__styles()` at module scope *without* its own\n * `'use client'` directive, even though `__styles` itself is client-only.\n * Rendering the icon from a Server Component pulls that module into the\n * server's RSC graph, and `next build` (Turbopack) fails collecting page\n * data with \"Attempted to call __styles() from the server but __styles is\n * on the client\" — reproduced against `@fluentui/react-icons@2.0.333` /\n * `@griffel/react@1.7.5`, and confirmed to affect every route sharing\n * Turbopack's chunk for this icon, not just this one. `\"use client\"` here\n * keeps the icon import inside a client boundary so it's never evaluated on\n * the server. `select.tsx` and `checkbox.tsx` carry the same fix for the\n * same reason.\n *\n * ## Keyboard behavior note\n * Base UI's Accordion no longer implements roving-tabindex arrow-key\n * navigation between triggers — `AccordionRoot.Props.orientation` is\n * explicitly documented (and typed) as `@deprecated`, \"following the APG\n * guidance update to remove the roving focus pattern\" for accordions, and\n * the compiled `AccordionTrigger` has no `onKeyDown` handler beyond the\n * generic focusable-when-disabled Tab guard — only `onClick`. So each\n * trigger is a normal Tab stop (Enter/Space activate it, matching any\n * `<button>`); ArrowUp/ArrowDown do **not** move focus between triggers in\n * this version, and the test file asserts that actual (non-roving)\n * behavior rather than the older Radix pattern.\n */\nfunction Accordion<Value = unknown>(\n  props: AccordionPrimitive.Root.Props<Value>\n) {\n  return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />;\n}\n\n/**\n * Item — groups one trigger with its panel. `border-b` per shadcn (every\n * item but the last is visually separated by the next item's top-less\n * border — the whole accordion typically sits inside a container that adds\n * the top border, matching shadcn's own layout).\n */\nfunction AccordionItem({\n  className,\n  ...props\n}: ComponentProps<typeof AccordionPrimitive.Item>) {\n  return (\n    <AccordionPrimitive.Item\n      data-slot=\"accordion-item\"\n      className={cn(\"border-b border-border last:border-b-0\", className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * Trigger — `Accordion.Header` (renders `<h3>`, the heading level shadcn\n * uses) wrapping `Accordion.Trigger` (the actual `<button>`), exactly like\n * shadcn's Radix-based composition. The chevron rotates via\n * `group-data-[panel-open]:rotate-180` — see the component doc comment for\n * why the attribute lives on the trigger rather than the icon itself.\n */\nfunction AccordionTrigger({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof AccordionPrimitive.Trigger>) {\n  return (\n    <AccordionPrimitive.Header data-slot=\"accordion-header\" className=\"flex\">\n      <AccordionPrimitive.Trigger\n        data-slot=\"accordion-trigger\"\n        className={cn(\n          // py-3 + 20px line-height = Fluent's 44px Medium header; title is\n          // Body 1 Regular (400), not Medium — same fix class as Label (M6).\n          // (Figma validation pass 2, nodes 9074:915/9074:921.)\n          \"group flex flex-1 items-center justify-between gap-2 py-3 text-left text-sm font-normal outline-none\",\n          \"transition-colors duration-fast ease-ease hover:underline\",\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          \"disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        <ChevronDownRegular\n          aria-hidden=\"true\"\n          className=\"size-4 shrink-0 text-muted-foreground transition-transform duration-normal ease-ease group-data-[panel-open]:rotate-180\"\n        />\n      </AccordionPrimitive.Trigger>\n    </AccordionPrimitive.Header>\n  );\n}\n\n/**\n * Content — the collapsible panel. See the component doc comment's \"Height\n * animation\" section for the exact mechanism. `role=\"region\"` and\n * `aria-labelledby` (pointing at the trigger) are set by Base UI itself, not\n * reimplemented here.\n */\nfunction AccordionContent({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof AccordionPrimitive.Panel>) {\n  return (\n    <AccordionPrimitive.Panel\n      data-slot=\"accordion-content\"\n      className={cn(\n        \"h-[var(--accordion-panel-height)] overflow-hidden text-sm\",\n        \"transition-[height] duration-normal ease-decelerate-mid\",\n        \"data-starting-style:h-0 data-ending-style:h-0 data-ending-style:ease-accelerate-mid\"\n      )}\n      {...props}\n    >\n      <div className={cn(\"pb-4 text-muted-foreground\", className)}>\n        {children}\n      </div>\n    </AccordionPrimitive.Panel>\n  );\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
    }
  ],
  "docs": "Base UI's Accordion has no `type=\"single\" | \"multiple\"` prop like Radix — it exposes a single `multiple?: boolean` prop (default `false`) and always represents the open set as an array (`value`/`defaultValue` are `Value[]`, even in single mode). `<Accordion>` passes `Accordion.Root`'s props straight through rather than papering over that difference; see `accordion.tsx`'s doc comment for the full divergence list, the exact `--accordion-panel-height` height-animation mechanism, and why arrow keys no longer move focus between triggers (Base UI dropped the roving-tabindex pattern per the updated ARIA APG guidance)."
}
