{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"accordion","type":"registry:component","title":"Accordion","description":"Expanding sections that grow to their natural height with no measuring, with arrow key navigation and closed panels skipped by Tab.","author":"Ryan","dependencies":["clsx","lucide-react","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/accordion.tsx","type":"registry:component","target":"@components/motion/accordion.tsx","content":"\"use client\";\n// easeui.dev/components/motion/accordion\n\nimport { ChevronDown } from \"lucide-react\";\nimport {\n  createContext,\n  type KeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype AccordionContextValue = {\n  isOpen: (value: string) => boolean;\n  toggle: (value: string) => void;\n};\n\ntype ItemContextValue = { value: string; open: boolean; triggerId: string; panelId: string };\n\nconst AccordionContext = createContext<AccordionContextValue | null>(null);\nconst ItemContext = createContext<ItemContextValue | null>(null);\n\nfunction useAccordionPart<T>(context: React.Context<T | null>, part: string): T {\n  const value = useContext(context);\n  if (!value) throw new Error(`${part} must be used inside an Accordion`);\n  return value;\n}\n\nexport interface AccordionProps {\n  /** Let several items stay open at once. Default false. */\n  multiple?: boolean;\n  /** Open items when uncontrolled. */\n  defaultValue?: string[];\n  /** Open items when controlled. */\n  value?: string[];\n  /** Called with the open items after each change. */\n  onValueChange?: (value: string[]) => void;\n  className?: string;\n  children: ReactNode;\n}\n\n/** Arrow keys, Home, and End move between triggers, like a list of tabs. */\nfunction moveFocus(event: KeyboardEvent<HTMLDivElement>) {\n  const keys = [\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"];\n  const target = event.target as HTMLElement;\n  if (!keys.includes(event.key) || !target.matches(\"[data-accordion-trigger]\")) return;\n  const triggers = Array.from(\n    event.currentTarget.querySelectorAll<HTMLButtonElement>(\"[data-accordion-trigger]:not(:disabled)\"),\n  );\n  const index = triggers.indexOf(target as HTMLButtonElement);\n  const next =\n    event.key === \"Home\"\n      ? 0\n      : event.key === \"End\"\n        ? triggers.length - 1\n        : (index + (event.key === \"ArrowDown\" ? 1 : -1) + triggers.length) % triggers.length;\n  event.preventDefault();\n  triggers[next]?.focus();\n}\n\nexport function Accordion({\n  multiple = false,\n  defaultValue = [],\n  value,\n  onValueChange,\n  className,\n  children,\n}: AccordionProps) {\n  const [uncontrolled, setUncontrolled] = useState(defaultValue);\n  const openValues = value ?? uncontrolled;\n\n  const toggle = useCallback(\n    (item: string) => {\n      const isOpen = openValues.includes(item);\n      const next = isOpen\n        ? openValues.filter((v) => v !== item)\n        : multiple\n          ? [...openValues, item]\n          : [item];\n      if (value === undefined) setUncontrolled(next);\n      onValueChange?.(next);\n    },\n    [multiple, onValueChange, openValues, value],\n  );\n\n  const context = useMemo(\n    () => ({ isOpen: (item: string) => openValues.includes(item), toggle }),\n    [openValues, toggle],\n  );\n\n  return (\n    <AccordionContext.Provider value={context}>\n      {/* biome-ignore lint/a11y/noStaticElementInteractions: key handling only moves focus between the trigger buttons inside. */}\n      <div onKeyDown={moveFocus} className={cn(\"flex flex-col\", className)}>\n        {children}\n      </div>\n    </AccordionContext.Provider>\n  );\n}\n\nexport interface AccordionItemProps {\n  /** Unique id for this item within the accordion. */\n  value: string;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function AccordionItem({ value, className, children }: AccordionItemProps) {\n  const { isOpen } = useAccordionPart(AccordionContext, \"AccordionItem\");\n  const id = useId();\n  const open = isOpen(value);\n  const item = useMemo(\n    () => ({ value, open, triggerId: `${id}-trigger`, panelId: `${id}-panel` }),\n    [id, open, value],\n  );\n\n  return (\n    <ItemContext.Provider value={item}>\n      <div data-state={open ? \"open\" : \"closed\"} className={cn(\"border-b border-border\", className)}>\n        {children}\n      </div>\n    </ItemContext.Provider>\n  );\n}\n\nexport interface AccordionTriggerProps {\n  className?: string;\n  disabled?: boolean;\n  children: ReactNode;\n}\n\nexport function AccordionTrigger({ className, disabled, children }: AccordionTriggerProps) {\n  const { toggle } = useAccordionPart(AccordionContext, \"AccordionTrigger\");\n  const { value, open, triggerId, panelId } = useAccordionPart(ItemContext, \"AccordionTrigger\");\n\n  return (\n    <h3 className=\"flex\">\n      <button\n        id={triggerId}\n        type=\"button\"\n        data-accordion-trigger=\"\"\n        aria-expanded={open}\n        aria-controls={panelId}\n        disabled={disabled}\n        onClick={() => toggle(value)}\n        className={cn(\n          \"group flex min-h-12 flex-1 touch-manipulation items-center justify-between gap-4 py-3 text-left text-sm font-medium text-foreground outline-none\",\n          \"focus-visible:rounded-md focus-visible:ring-2 focus-visible:ring-foreground/40\",\n          \"disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n      >\n        {children}\n        <ChevronDown\n          aria-hidden=\"true\"\n          className={cn(\n            \"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 ease-out motion-reduce:transition-none\",\n            open && \"rotate-180\",\n          )}\n        />\n      </button>\n    </h3>\n  );\n}\n\nexport interface AccordionContentProps {\n  className?: string;\n  children: ReactNode;\n}\n\n/**\n * The panel grows from zero rows to its full height with a grid transition,\n * so there is nothing to measure. Closed panels are inert, so Tab skips them.\n */\nexport function AccordionContent({ className, children }: AccordionContentProps) {\n  const { open, triggerId, panelId } = useAccordionPart(ItemContext, \"AccordionContent\");\n\n  return (\n    <section\n      id={panelId}\n      aria-labelledby={triggerId}\n      inert={!open}\n      className={cn(\n        \"grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none\",\n        open ? \"grid-rows-[1fr]\" : \"grid-rows-[0fr]\",\n      )}\n    >\n      <div className=\"overflow-hidden\">\n        <div\n          className={cn(\n            \"pb-4 text-sm leading-6 text-muted-foreground transition-opacity duration-200 ease-out motion-reduce:transition-none\",\n            open ? \"opacity-100\" : \"opacity-0\",\n            className,\n          )}\n        >\n          {children}\n        </div>\n      </div>\n    </section>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}