{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"accordion","type":"registry:component","title":"Accordion","description":"Sections that open in place: the height eases open, the content rises out of a blur a beat later, and opening one folds the other at the same time.","author":"Hairline UI","dependencies":[],"registryDependencies":["https://hairlineui.com/r/ui-interaction-motion.json?as=dependency"],"files":[{"path":"registry/accordion.tsx","type":"registry:component","content":"// Hairline UI · accordion · MIT License\n// Copyright (c) 2026 Hairline UI · https://hairlineui.com/license#components\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { EASE_OUT, INTERACTION_FOCUS, useInteractionReducedMotion } from \"./ui-interaction-motion\";\n\n/* ─────────────────────────────────────────────────────────\n * ACCORDION: sections that open in place\n *\n *   open     the section's height eases open (0fr → 1fr) while its\n *            content rises out of a blur a beat later; the chevron\n *            turns over in the same motion\n *   single   opening one folds the open one at the same time, so\n *            the page below barely moves\n *   keys     ↑/↓ move between headers, Home/End jump; Enter and\n *            Space open\n *\n * For FAQ sections on landing pages there is faq-accordion; this\n * is the plain one for settings, filters and details.\n * ───────────────────────────────────────────────────────── */\n\nexport interface AccordionItem {\n  value: string;\n  title: React.ReactNode;\n  /** A second line under the title. */\n  subtitle?: React.ReactNode;\n  content: React.ReactNode;\n  disabled?: boolean;\n}\n\nexport interface AccordionProps {\n  items: AccordionItem[];\n  /** One open at a time, or any number. */\n  type?: \"single\" | \"multiple\";\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n  /** Hairline rows, or each section as its own card. */\n  variant?: \"rows\" | \"cards\";\n  className?: string;\n}\n\n/** The fold every disclosure in the library uses: height by grid rows, content through a blur. */\nexport function Fold({ open, children, reduced, id, labelledBy }: { open: boolean; children: React.ReactNode; reduced: boolean; id?: string; labelledBy?: string }) {\n  return (\n    <div\n      id={id}\n      role={labelledBy ? \"region\" : undefined}\n      aria-labelledby={labelledBy}\n      inert={!open || undefined}\n      className=\"grid\"\n      style={{ gridTemplateRows: open ? \"1fr\" : \"0fr\", transition: reduced ? \"none\" : `grid-template-rows 420ms ${EASE_OUT}` }}\n    >\n      <div className=\"min-h-0 overflow-hidden\">\n        <div\n          style={{\n            opacity: open ? 1 : 0,\n            filter: open || reduced ? \"blur(0px)\" : \"blur(4px)\",\n            translate: open || reduced ? \"0 0\" : \"0 -6px\",\n            transition: reduced ? \"none\" : open ? `opacity 300ms ${EASE_OUT} 90ms, filter 300ms ${EASE_OUT} 90ms, translate 420ms ${EASE_OUT}` : `opacity 160ms ${EASE_OUT}, filter 160ms ${EASE_OUT}, translate 260ms ${EASE_OUT}`,\n          }}\n        >\n          {children}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function Chevron({ open, reduced }: { open: boolean; reduced: boolean }) {\n  return (\n    <svg\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n      className=\"size-3.5 shrink-0 text-muted-foreground\"\n      style={{ transform: open ? \"rotate(180deg)\" : \"none\", transition: reduced ? \"none\" : `transform 420ms ${EASE_OUT}` }}\n    >\n      <path d=\"M4 6l4 4 4-4\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nexport function Accordion({ items, type = \"single\", value, defaultValue = [], onValueChange, variant = \"rows\", className = \"\" }: AccordionProps) {\n  const reduced = useInteractionReducedMotion();\n  const baseId = React.useId();\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const [own, setOwn] = React.useState(defaultValue);\n  const open = value ?? own;\n  const cards = variant === \"cards\";\n\n  const toggle = (v: string) => {\n    const next = open.includes(v) ? open.filter((x) => x !== v) : type === \"single\" ? [v] : [...open, v];\n    if (value === undefined) setOwn(next);\n    onValueChange?.(next);\n  };\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    const heads = Array.from(rootRef.current?.querySelectorAll<HTMLButtonElement>(\"[data-accordion-head]:not(:disabled)\") ?? []);\n    const i = heads.indexOf(document.activeElement as HTMLButtonElement);\n    if (i < 0) return;\n    let next = -1;\n    if (e.key === \"ArrowDown\") next = (i + 1) % heads.length;\n    else if (e.key === \"ArrowUp\") next = (i - 1 + heads.length) % heads.length;\n    else if (e.key === \"Home\") next = 0;\n    else if (e.key === \"End\") next = heads.length - 1;\n    if (next < 0) return;\n    e.preventDefault();\n    heads[next].focus();\n  };\n\n  return (\n    <div ref={rootRef} onKeyDown={onKeyDown} className={`flex w-full flex-col ${cards ? \"gap-2\" : \"divide-y divide-border border-y border-border\"} ${className}`}>\n      {items.map((item) => {\n        const on = open.includes(item.value);\n        const head = `${baseId}-h-${item.value}`;\n        const panel = `${baseId}-p-${item.value}`;\n        return (\n          <div key={item.value} className={cards ? `rounded-[16px] bg-card shadow-[inset_0_0_0_1px_var(--border)] transition-colors duration-300 ${on ? \"\" : \"hover:bg-accent/40\"}` : \"\"}>\n            <h3 className=\"m-0\">\n              <button\n                type=\"button\"\n                id={head}\n                data-accordion-head=\"\"\n                aria-expanded={on}\n                aria-controls={panel}\n                disabled={item.disabled}\n                onClick={() => toggle(item.value)}\n                className={`flex w-full items-center gap-3 text-left cursor-pointer disabled:cursor-default disabled:opacity-40 ${cards ? \"rounded-[16px] px-4 py-3.5\" : \"py-3.5\"} ${INTERACTION_FOCUS} focus-visible:ring-offset-0`}\n              >\n                <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n                  <span className=\"text-[14px] font-medium text-foreground\">{item.title}</span>\n                  {item.subtitle && <span className=\"text-[12.5px] text-muted-foreground\">{item.subtitle}</span>}\n                </span>\n                <Chevron open={on} reduced={reduced} />\n              </button>\n            </h3>\n            <Fold open={on} reduced={reduced} id={panel} labelledBy={head}>\n              <div className={`text-[13.5px] leading-relaxed text-muted-foreground ${cards ? \"px-4 pb-4\" : \"pb-4\"}`}>{item.content}</div>\n            </Fold>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\nexport default Accordion;\n"}],"css":{},"docs":"Docs and live preview: https://hairlineui.com/ui/accordion. MIT licensed: https://hairlineui.com/license#components","meta":{"license":"MIT"}}