{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "accordion",
  "type": "registry:ui",
  "title": "Accordion",
  "description": "Progressively reveal supporting information in place.",
  "categories": [
    "disclosure"
  ],
  "dependencies": [
    "@radix-ui/react-accordion",
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://mcp.xzero.co.uk/r/xzero-motion-tokens.json"
  ],
  "files": [
    {
      "path": "registry/components/accordion/accordion.tsx",
      "type": "registry:component",
      "target": "@components/xzero/accordion/accordion.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDown } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { TargetAndTransition, Variants } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport styles from \"./accordion.module.css\";\n\nexport interface AccordionItem { title: string; content: ReactNode; }\nexport interface AccordionProps {\n  items: AccordionItem[];\n  defaultOpen?: number;\n  /** \"lg\" suits page-level FAQs: questions at the large text size, answers at body size. */\n  size?: \"md\" | \"lg\";\n}\n\n/** Height follows the content on a spring that never overshoots; closed panels leave the accessibility tree once they finish collapsing. */\nconst panelOpen: TargetAndTransition = { height: \"auto\", opacity: 1, visibility: \"visible\" };\nconst panelClosed: TargetAndTransition = { height: 0, opacity: 0, transitionEnd: { visibility: \"hidden\" } };\n/** The answer settles down into place with a brief focus pull as the panel opens. */\nconst contentOpen: TargetAndTransition = { y: 0, filter: \"blur(0px)\", transitionEnd: { filter: \"none\" } };\nconst contentClosed: TargetAndTransition = { y: -6, filter: `blur(${motionTokens.blur.subtle}px)` };\nconst panelMotion: Variants = {\n  open: { ...panelOpen, transition: { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } } },\n  closed: { ...panelClosed, transition: { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } } },\n};\nconst contentMotion: Variants = {\n  open: { ...contentOpen, transition: { y: motionTokens.spring.smooth, filter: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } } },\n  closed: { ...contentClosed, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } },\n};\n/** Reduced motion: same end states in one step, so server and client render identical styles. */\nconst panelStill: Variants = { open: { ...panelOpen, transition: { duration: 0 } }, closed: { ...panelClosed, transition: { duration: 0 } } };\nconst contentStill: Variants = { open: { ...contentOpen, transition: { duration: 0 } }, closed: { ...contentClosed, transition: { duration: 0 } } };\n\nexport function Accordion({ items, defaultOpen = 0, size = \"md\" }: AccordionProps) {\n  const initialValue = defaultOpen >= 0 && defaultOpen < items.length ? String(defaultOpen) : \"\";\n  const [openValue, setOpenValue] = useState(initialValue);\n  const reduced = useReducedMotion();\n  return <AccordionPrimitive.Root className={[styles.accordion, size === \"lg\" ? styles.lg : \"\"].filter(Boolean).join(\" \")} type=\"single\" collapsible value={openValue} onValueChange={setOpenValue}>\n    {items.map((item, index) => { const open = openValue === String(index); return <AccordionPrimitive.Item className={styles.item} value={String(index)} key={`${item.title}-${index}`}>\n      <AccordionPrimitive.Header className={styles.header}>\n        <AccordionPrimitive.Trigger className={styles.trigger}>\n          <span>{item.title}</span><motion.span className={styles.icon} initial={false} animate={{ rotate: open ? 180 : 0 }} transition={reduced ? { duration: 0 } : motionTokens.spring.snappy}><ChevronDown width={17} height={17} aria-hidden=\"true\" /></motion.span>\n        </AccordionPrimitive.Trigger>\n      </AccordionPrimitive.Header>\n      {/* Radix keeps semantics and ids; motion owns the height so a toggle mid-flight retargets instead of restarting. */}\n      <AccordionPrimitive.Content forceMount asChild>\n        <motion.div className={styles.panel} initial={false} animate={open ? \"open\" : \"closed\"} variants={reduced ? panelStill : panelMotion}>\n          <motion.div className={styles.panelInner} variants={reduced ? contentStill : contentMotion}>{item.content}</motion.div>\n        </motion.div>\n      </AccordionPrimitive.Content>\n    </AccordionPrimitive.Item>; })}\n  </AccordionPrimitive.Root>;\n}\n\nexport default Accordion;\n"
    },
    {
      "path": "registry/components/accordion/accordion.module.css",
      "type": "registry:component",
      "target": "@components/xzero/accordion/accordion.module.css",
      "content": ".accordion { width: 100%; border-top: 1px solid var(--border); }\n.item { border-bottom: 1px solid var(--border); }\n.header { margin: 0; }\n.trigger { display: flex; width: 100%; min-height: var(--control-height-lg); align-items: center; justify-content: space-between; gap: var(--space-5); padding: var(--space-2) 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }\n@media (hover: hover) and (pointer: fine) { .trigger:hover { color: var(--text-secondary); } .trigger:hover .icon { color: var(--foreground); } }\n/* Rotation is driven by a spring in accordion.tsx; CSS only handles color. */\n.icon { display: inline-flex; flex: 0 0 auto; color: var(--text-muted); transition: color var(--duration-fast) var(--ease-standard); }\n.trigger[data-state=\"open\"] .icon { color: var(--foreground); }\n.panel { overflow: hidden; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }\n.panelInner { overflow-wrap: anywhere; padding: 0 var(--space-8) var(--space-5) 0; }\n.lg .trigger { min-height: 76px; gap: var(--space-8); padding-block: var(--space-5); font-size: var(--text-lg); letter-spacing: var(--tracking-body); }\n.lg .panel { font-size: var(--text-base); line-height: 1.65; }\n.lg .panelInner { max-width: 62ch; padding: 0 var(--space-12) var(--space-6) 0; }\n@media (max-width: 520px) { .lg .trigger { min-height: 68px; font-size: var(--text-base); } .lg .panel { font-size: var(--text-sm); } .lg .panelInner { padding-right: var(--space-6); } }\n@media (prefers-reduced-motion: reduce) { .trigger, .icon { transition: none; } }\n"
    }
  ],
  "docs": "Docs and live preview: https://ui.xzero.co.uk/components/accordion",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "disclosure",
      "layout"
    ],
    "docs": "https://ui.xzero.co.uk/components/accordion",
    "markdown": "https://ui.xzero.co.uk/components/accordion/markdown"
  }
}