{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"accordion","type":"registry:ui","title":"Accordion","description":"Stacked disclosure sections with smooth auto-height animation, a springy chevron and full arrow/Home/End keyboard support.","dependencies":["lucide-react"],"registryDependencies":["utils"],"files":[{"path":"registry/ui/accordion.tsx","type":"registry:ui","content":"\"use client\";\n\nimport {\n  createContext,\n  useContext,\n  useId,\n  useRef,\n  useState,\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ntype RootCtx = { open: string[]; toggle: (value: string) => void };\ntype ItemCtx = { value: string; open: boolean; disabled: boolean; triggerId: string; contentId: string };\n\nconst AccordionContext = createContext<RootCtx | null>(null);\nconst ItemContext = createContext<ItemCtx | null>(null);\n\nfunction useItem() {\n  const ctx = useContext(ItemContext);\n  if (!ctx) throw new Error(\"Accordion parts must be rendered inside <AccordionItem>.\");\n  return ctx;\n}\n\nexport interface AccordionProps extends Omit<HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {\n  /** `single` keeps at most one item open, `multiple` lets any number stay open. */\n  type?: \"single\" | \"multiple\";\n  /** In single mode, allow closing the open item so none are open. */\n  collapsible?: boolean;\n  /** Open item values (controlled). */\n  value?: string[];\n  /** Open item values on first render (uncontrolled). */\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n}\n\nexport function Accordion({\n  type = \"single\",\n  collapsible = true,\n  value,\n  defaultValue = [],\n  onValueChange,\n  className,\n  onKeyDown,\n  children,\n  ...props\n}: AccordionProps) {\n  const [inner, setInner] = useState(defaultValue);\n  const open = value ?? inner;\n  const root = useRef<HTMLDivElement>(null);\n\n  const toggle = (v: string) => {\n    const isOpen = open.includes(v);\n    let next: string[];\n    if (type === \"multiple\") next = isOpen ? open.filter((x) => x !== v) : [...open, v];\n    else if (isOpen) next = collapsible ? [] : open;\n    else next = [v];\n    if (value === undefined) setInner(next);\n    onValueChange?.(next);\n  };\n\n  // WAI-ARIA accordion: arrows move between headers, Home/End jump to the ends.\n  const onKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(e);\n    const el = root.current;\n    if (!el || !(e.target as HTMLElement).matches(\"[data-accordion-trigger]\")) return;\n    const triggers = [...el.querySelectorAll<HTMLButtonElement>(\"[data-accordion-trigger]:not(:disabled)\")].filter(\n      (t) => t.closest(\"[data-accordion]\") === el,\n    );\n    const i = triggers.indexOf(e.target as HTMLButtonElement);\n    const n = triggers.length;\n    const next = { ArrowDown: i + 1, ArrowUp: i - 1 + n, Home: 0, End: n - 1 }[e.key];\n    if (next === undefined || i < 0) return;\n    e.preventDefault();\n    triggers[next % n].focus();\n  };\n\n  return (\n    <AccordionContext value={{ open, toggle }}>\n      <div {...props} ref={root} data-accordion=\"\" onKeyDown={onKey} className={cn(\"w-full\", className)}>\n        {children}\n      </div>\n    </AccordionContext>\n  );\n}\n\nexport interface AccordionItemProps extends HTMLAttributes<HTMLDivElement> {\n  /** Unique id of this item within the accordion. */\n  value: string;\n  disabled?: boolean;\n}\n\nexport function AccordionItem({ value, disabled = false, className, children, ...props }: AccordionItemProps) {\n  const ctx = useContext(AccordionContext);\n  if (!ctx) throw new Error(\"<AccordionItem> must be rendered inside <Accordion>.\");\n  const id = useId();\n  const open = ctx.open.includes(value);\n  return (\n    <ItemContext value={{ value, open, disabled, triggerId: `${id}-t`, contentId: `${id}-c` }}>\n      <div\n        {...props}\n        data-state={open ? \"open\" : \"closed\"}\n        className={cn(\"border-b border-border last:border-b-0\", className)}\n      >\n        {children}\n      </div>\n    </ItemContext>\n  );\n}\n\nexport interface AccordionTriggerProps extends HTMLAttributes<HTMLButtonElement> {\n  /** Heading level wrapping the button, per the WAI-ARIA accordion pattern. */\n  level?: 2 | 3 | 4 | 5 | 6;\n  /** Replaces the default chevron. */\n  icon?: ReactNode;\n}\n\nexport function AccordionTrigger({ level = 3, icon, className, children, onClick, ...props }: AccordionTriggerProps) {\n  const root = useContext(AccordionContext)!;\n  const { value, open, disabled, triggerId, contentId } = useItem();\n  const Heading = `h${level}` as const;\n  return (\n    <Heading className=\"flex\">\n      <button\n        {...props}\n        type=\"button\"\n        id={triggerId}\n        data-accordion-trigger=\"\"\n        aria-expanded={open}\n        aria-controls={contentId}\n        disabled={disabled}\n        onClick={(e) => {\n          onClick?.(e);\n          if (!e.defaultPrevented) root.toggle(value);\n        }}\n        className={cn(\n          \"group flex flex-1 items-center justify-between gap-4 rounded-md py-4 text-left text-sm font-medium text-foreground outline-none transition-colors\",\n          \"hover:text-foreground/80 focus-visible:ring-2 focus-visible:ring-ring/60 disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n      >\n        {children}\n        <span\n          aria-hidden\n          className={cn(\n            \"grid size-6 shrink-0 place-items-center rounded-full text-muted-foreground transition-[rotate,background-color,color] duration-300 ease-[cubic-bezier(.3,1.4,.5,1)] motion-reduce:transition-none\",\n            \"group-hover:bg-muted group-hover:text-foreground\",\n            open && \"rotate-180 bg-muted text-foreground\",\n          )}\n        >\n          {icon ?? <ChevronDown className=\"size-4\" />}\n        </span>\n      </button>\n    </Heading>\n  );\n}\n\nexport function AccordionContent({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {\n  const { open, triggerId, contentId } = useItem();\n  return (\n    // Animating grid rows 0fr -> 1fr gives a smooth auto-height transition without measuring.\n    <div\n      {...props}\n      id={contentId}\n      role=\"region\"\n      aria-labelledby={triggerId}\n      inert={!open}\n      data-state={open ? \"open\" : \"closed\"}\n      className=\"grid transition-[grid-template-rows] duration-300 ease-[cubic-bezier(.2,.8,.2,1)] motion-reduce:transition-none\"\n      style={{ gridTemplateRows: open ? \"1fr\" : \"0fr\" }}\n    >\n      <div className=\"overflow-hidden\">\n        <div\n          className={cn(\n            \"pb-4 text-sm leading-relaxed text-muted-foreground transition-[opacity,translate] duration-300 motion-reduce:transition-none\",\n            open ? \"translate-y-0 opacity-100\" : \"-translate-y-1 opacity-0\",\n            className,\n          )}\n        >\n          {children}\n        </div>\n      </div>\n    </div>\n  );\n}\n"}]}