{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "accordion",
  "title": "Accordion",
  "description": "A disclosure list with spring height animation, rotating chevrons, and single or multiple open modes.",
  "dependencies": ["framer-motion"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/accordion/accordion.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport * as React from \"react\";\n\nexport type AccordionItem = {\n  value: string;\n  title: React.ReactNode;\n  content: React.ReactNode;\n  disabled?: boolean;\n};\n\n/**\n * Motion feel for the open/close animation.\n * - `smooth`  critically damped — no overshoot (default)\n * - `spring`  gentle overshoot with a soft settle\n * - `bounce`  playful — content springs into place\n *\n * The container height is always kept near-critically damped: overshooting an\n * `auto` height flashes an empty gap below the content, so the springiness\n * lives on the content lift (`y`) instead, where it reads well and clips\n * nothing.\n */\nexport type AccordionAnimation = \"smooth\" | \"spring\" | \"bounce\";\n\nconst MOTION_PRESETS: Record<\n  AccordionAnimation,\n  {\n    height: { bounce: number; duration: number };\n    content: { bounce: number; duration: number };\n    lift: number;\n  }\n> = {\n  smooth: {\n    height: { bounce: 0, duration: 0.4 },\n    content: { bounce: 0, duration: 0.4 },\n    lift: 6,\n  },\n  spring: {\n    height: { bounce: 0.05, duration: 0.45 },\n    content: { bounce: 0.3, duration: 0.5 },\n    lift: 10,\n  },\n  bounce: {\n    height: { bounce: 0.12, duration: 0.5 },\n    content: { bounce: 0.55, duration: 0.6 },\n    lift: 14,\n  },\n};\n\nexport type AccordionProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onChange\" | \"defaultValue\"\n> & {\n  items: AccordionItem[];\n  /** `single` keeps one panel open; `multiple` allows many. */\n  type?: \"single\" | \"multiple\";\n  /** Open value(s) on mount. */\n  defaultValue?: string | string[];\n  /** Allow closing the open panel in `single` mode. */\n  collapsible?: boolean;\n  /** Motion feel for the height animation. */\n  animation?: AccordionAnimation;\n};\n\nconst ChevronIcon = (\n  <svg\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth={2}\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className=\"size-4 shrink-0 text-muted-foreground [transition:transform_250ms_ease] group-data-[open=true]:rotate-180\"\n    aria-hidden=\"true\"\n  >\n    <path d=\"m6 9 6 6 6-6\" />\n  </svg>\n);\n\nconst Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(\n  (\n    {\n      items,\n      type = \"single\",\n      defaultValue,\n      collapsible = true,\n      animation = \"smooth\",\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const reduceMotion = useReducedMotion();\n    const preset = MOTION_PRESETS[animation];\n    const heightSpring = { type: \"spring\" as const, ...preset.height };\n    const contentSpring = { type: \"spring\" as const, ...preset.content };\n    const [open, setOpen] = React.useState<string[]>(() => {\n      if (defaultValue === undefined) return [];\n      return Array.isArray(defaultValue) ? defaultValue : [defaultValue];\n    });\n\n    const toggle = (value: string) => {\n      setOpen((current) => {\n        const isOpen = current.includes(value);\n        if (type === \"single\") {\n          if (isOpen) return collapsible ? [] : current;\n          return [value];\n        }\n        return isOpen\n          ? current.filter((v) => v !== value)\n          : [...current, value];\n      });\n    };\n\n    return (\n      <div\n        ref={ref}\n        className={`w-full divide-y divide-border overflow-hidden rounded-xl border border-border ${\n          className ?? \"\"\n        }`}\n        {...props}\n      >\n        {items.map((item) => {\n          const isOpen = open.includes(item.value);\n          const panelId = `accordion-panel-${item.value}`;\n          const triggerId = `accordion-trigger-${item.value}`;\n          return (\n            <div key={item.value} className=\"group\" data-open={isOpen}>\n              <h3 className=\"flex\">\n                <button\n                  type=\"button\"\n                  id={triggerId}\n                  aria-expanded={isOpen}\n                  aria-controls={panelId}\n                  disabled={item.disabled}\n                  onClick={() => toggle(item.value)}\n                  className=\"flex flex-1 items-center justify-between gap-4 px-5 py-4 text-left text-sm font-medium text-foreground [transition:background_150ms_ease] group-data-[open=false]:hover:bg-accent/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\"\n                >\n                  {item.title}\n                  {ChevronIcon}\n                </button>\n              </h3>\n              <AnimatePresence initial={false}>\n                {isOpen && (\n                  <motion.div\n                    id={panelId}\n                    role=\"region\"\n                    aria-labelledby={triggerId}\n                    key=\"content\"\n                    initial={reduceMotion ? false : { height: 0, opacity: 0 }}\n                    animate={{ height: \"auto\", opacity: 1 }}\n                    exit={reduceMotion ? undefined : { height: 0, opacity: 0 }}\n                    transition={{\n                      height: heightSpring,\n                      opacity: { duration: 0.2 },\n                    }}\n                    className=\"overflow-hidden\"\n                  >\n                    <motion.div\n                      initial={reduceMotion ? false : { y: -preset.lift }}\n                      animate={{ y: 0 }}\n                      transition={\n                        reduceMotion\n                          ? undefined\n                          : { ...contentSpring, delay: 0.03 }\n                      }\n                      className=\"px-5 pb-4 pt-0 text-sm text-muted-foreground [text-wrap:pretty]\"\n                    >\n                      {item.content}\n                    </motion.div>\n                  </motion.div>\n                )}\n              </AnimatePresence>\n            </div>\n          );\n        })}\n      </div>\n    );\n  },\n);\nAccordion.displayName = \"Accordion\";\n\nexport { Accordion };\n",
      "type": "registry:ui",
      "target": "components/godui/accordion.tsx"
    }
  ],
  "type": "registry:ui"
}
