{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"accordion","type":"registry:component","title":"Accordion","description":"Calm shadcn-compatible accordion built on Collapsible (Accordion, AccordionItem, AccordionTrigger, AccordionContent) with type single or multiple, collapsible, controlled or uncontrolled value, a spring-rotating chevron, height-morphing panels and Arrow/Home/End focus movement between triggers; the quiet counterpart to the springy Bouncy Accordion.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/accordion.tsx","type":"registry:component","target":"@components/motion/accordion.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/motion/accordion\n\nimport { ChevronDown } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useId,\n  useState,\n  type ButtonHTMLAttributes,\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n  useCollapsible,\n} from \"@/components/motion/collapsible\";\nimport { SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\n// Accordion is the calm, shadcn-compatible stack of disclosures built from\n// `Collapsible`. `BouncyAccordion` is the springy single-open variant with icon\n// rows; `FaqSection` is a full marketing block. Use this one anywhere a plain\n// accordion belongs (settings groups, FAQs, docs sidebars).\n\ninterface AccordionContextValue {\n  value: string[];\n  toggle: (item: string) => void;\n  type: \"single\" | \"multiple\";\n  collapsible: boolean;\n  disabled: boolean;\n  rootId: string;\n}\n\nconst AccordionContext = createContext<AccordionContextValue | null>(null);\n\nfunction useAccordion(component: string) {\n  const context = useContext(AccordionContext);\n  if (!context) throw new Error(`${component} must be used within <Accordion>.`);\n  return context;\n}\n\ninterface AccordionBaseProps\n  extends Omit<HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"dir\"> {\n  disabled?: boolean;\n  children?: ReactNode;\n}\n\nexport interface AccordionSingleProps extends AccordionBaseProps {\n  type: \"single\";\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Allow closing the open item so none is open. Default false (shadcn/Radix parity). */\n  collapsible?: boolean;\n}\n\nexport interface AccordionMultipleProps extends AccordionBaseProps {\n  type: \"multiple\";\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n  collapsible?: never;\n}\n\nexport type AccordionProps = AccordionSingleProps | AccordionMultipleProps;\n\nconst toArray = (v: string | string[] | undefined) =>\n  v === undefined ? undefined : Array.isArray(v) ? v : v ? [v] : [];\n\nexport const Accordion = forwardRef<HTMLDivElement, AccordionProps>(function Accordion(\n  props,\n  ref,\n) {\n  const {\n    type,\n    value: valueProp,\n    defaultValue,\n    onValueChange,\n    collapsible = false,\n    disabled = false,\n    className,\n    children,\n    ...rest\n  } = props as AccordionBaseProps & {\n    type: \"single\" | \"multiple\";\n    value?: string | string[];\n    defaultValue?: string | string[];\n    onValueChange?: ((value: string) => void) | ((value: string[]) => void);\n    collapsible?: boolean;\n  };\n\n  const [internal, setInternal] = useState<string[]>(() => toArray(defaultValue) ?? []);\n  const controlled = valueProp !== undefined;\n  const value = controlled ? (toArray(valueProp) ?? []) : internal;\n  const rootId = useId();\n\n  const toggle = useCallback(\n    (item: string) => {\n      const isOpen = value.includes(item);\n      let next: string[];\n      if (type === \"single\") {\n        if (isOpen && !collapsible) return;\n        next = isOpen ? [] : [item];\n      } else {\n        next = isOpen ? value.filter((v) => v !== item) : [...value, item];\n      }\n      if (!controlled) setInternal(next);\n      if (type === \"single\") {\n        (onValueChange as ((v: string) => void) | undefined)?.(next[0] ?? \"\");\n      } else {\n        (onValueChange as ((v: string[]) => void) | undefined)?.(next);\n      }\n    },\n    [value, type, collapsible, controlled, onValueChange],\n  );\n\n  return (\n    <AccordionContext.Provider\n      value={{ value, toggle, type, collapsible, disabled, rootId }}\n    >\n      <div\n        ref={ref}\n        data-orientation=\"vertical\"\n        data-accordion-root={rootId}\n        className={cn(\"w-full\", className)}\n        {...rest}\n      >\n        {children}\n      </div>\n    </AccordionContext.Provider>\n  );\n});\n\nexport interface AccordionItemProps extends HTMLAttributes<HTMLDivElement> {\n  value: string;\n  disabled?: boolean;\n  children?: ReactNode;\n}\n\nexport const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(\n  function AccordionItem({ value, disabled = false, className, children, ...rest }, ref) {\n    const accordion = useAccordion(\"AccordionItem\");\n    const open = accordion.value.includes(value);\n    const itemDisabled = accordion.disabled || disabled;\n\n    return (\n      <Collapsible\n        ref={ref}\n        open={open}\n        onOpenChange={() => accordion.toggle(value)}\n        disabled={itemDisabled}\n        className={cn(\"border-b border-border\", className)}\n        {...rest}\n      >\n        {children}\n      </Collapsible>\n    );\n  },\n);\n\nexport interface AccordionTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Replace the rotating chevron, or pass `null` to hide it. */\n  icon?: ReactNode;\n  /** Heading element wrapping the trigger (WAI-ARIA accordion pattern). */\n  headingLevel?: 2 | 3 | 4 | 5 | 6;\n  children?: ReactNode;\n}\n\nexport const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(\n  function AccordionTrigger(\n    { icon, headingLevel = 3, className, children, onKeyDown, ...rest },\n    ref,\n  ) {\n    const accordion = useAccordion(\"AccordionTrigger\");\n    const { open } = useCollapsible(\"AccordionTrigger\");\n    const reduce = useReducedMotion();\n    // A single, non-collapsible accordion can't close its open item, so the\n    // trigger reports that instead of silently ignoring the press.\n    const locked = accordion.type === \"single\" && !accordion.collapsible && open;\n    const Heading = `h${headingLevel}` as const;\n\n    const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n      onKeyDown?.(event);\n      if (event.defaultPrevented) return;\n      const keys = [\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"];\n      if (!keys.includes(event.key)) return;\n      const root = event.currentTarget.closest<HTMLElement>(\n        `[data-accordion-root=\"${accordion.rootId}\"]`,\n      );\n      const scope = root ?? document;\n      const triggers = Array.from(\n        scope.querySelectorAll<HTMLButtonElement>(\n          `[data-accordion-trigger=\"${accordion.rootId}\"]`,\n        ),\n      ).filter((el) => !el.disabled);\n      const index = triggers.indexOf(event.currentTarget);\n      if (index < 0 || triggers.length === 0) return;\n      event.preventDefault();\n      const last = triggers.length - 1;\n      const next =\n        event.key === \"Home\"\n          ? 0\n          : event.key === \"End\"\n            ? last\n            : event.key === \"ArrowDown\"\n              ? (index + 1) % triggers.length\n              : (index - 1 + triggers.length) % triggers.length;\n      triggers[next]?.focus();\n    };\n\n    return (\n      <Heading className=\"flex\">\n        <CollapsibleTrigger\n          ref={ref}\n          data-accordion-trigger={accordion.rootId}\n          aria-disabled={locked || undefined}\n          className={cn(\n            \"group/accordion-trigger 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/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n            \"disabled:cursor-not-allowed disabled:opacity-50\",\n            className,\n          )}\n          onKeyDown={handleKeyDown}\n          {...rest}\n        >\n          {children}\n          {icon === null ? null : (\n            <motion.span\n              aria-hidden\n              className=\"grid size-4 shrink-0 place-items-center text-muted-foreground\"\n              initial={false}\n              animate={{ rotate: open ? 180 : 0 }}\n              transition={reduce ? { duration: 0 } : SPRING_SWAP}\n            >\n              {icon ?? <ChevronDown className=\"size-4\" />}\n            </motion.span>\n          )}\n        </CollapsibleTrigger>\n      </Heading>\n    );\n  },\n);\n\nexport interface AccordionContentProps extends HTMLAttributes<HTMLDivElement> {\n  children?: ReactNode;\n}\n\nexport function AccordionContent({ className, children, ...rest }: AccordionContentProps) {\n  useAccordion(\"AccordionContent\");\n  return (\n    <CollapsibleContent\n      region\n      innerClassName={cn(\"pb-4 text-sm leading-6 text-muted-foreground\", className)}\n      {...rest}\n    >\n      {children}\n    </CollapsibleContent>\n  );\n}\n"},{"path":"components/motion/collapsible.tsx","type":"registry:component","target":"@components/motion/collapsible.tsx","content":"\"use client\";\n\nimport { motion, useReducedMotion, type Transition } from \"motion/react\";\nimport {\n  cloneElement,\n  createContext,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useContext,\n  useId,\n  useState,\n  type ButtonHTMLAttributes,\n  type HTMLAttributes,\n  type MouseEvent,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\n// Collapsible is the unstyled disclosure primitive: one trigger, one panel.\n// `Accordion` (accordion.tsx) is built from it; `BouncyAccordion` is the\n// springy, single-open icon-row variant. Reach for this when a single region\n// should show or hide in flow (\"Show 3 more\", an expandable activity row).\n\n/** Entrance is a touch longer than the exit — exits should feel faster. */\nconst OPEN_TRANSITION: Transition = { duration: 0.28, ease: EASE_OUT };\nconst CLOSE_TRANSITION: Transition = { duration: 0.2, ease: EASE_OUT };\n\ninterface CollapsibleContextValue {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  disabled: boolean;\n  triggerId: string;\n  contentId: string;\n}\n\nconst CollapsibleContext = createContext<CollapsibleContextValue | null>(null);\n\nexport function useCollapsible(component = \"useCollapsible\") {\n  const context = useContext(CollapsibleContext);\n  if (!context) {\n    throw new Error(`${component} must be used within <Collapsible>.`);\n  }\n  return context;\n}\n\nexport interface CollapsibleProps\n  extends Omit<HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n  /** Controlled open state. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  disabled?: boolean;\n  /** Override the generated trigger id (the content is labelled by it). */\n  triggerId?: string;\n  /** Override the generated content id (the trigger controls it). */\n  contentId?: string;\n  children?: ReactNode;\n}\n\nexport const Collapsible = forwardRef<HTMLDivElement, CollapsibleProps>(\n  function Collapsible(\n    {\n      open: openProp,\n      defaultOpen = false,\n      onOpenChange,\n      disabled = false,\n      triggerId: triggerIdProp,\n      contentId: contentIdProp,\n      className,\n      children,\n      ...rest\n    },\n    ref,\n  ) {\n    const [internal, setInternal] = useState(defaultOpen);\n    const controlled = openProp !== undefined;\n    const open = controlled ? openProp : internal;\n    const setOpen = useCallback(\n      (next: boolean) => {\n        if (!controlled) setInternal(next);\n        onOpenChange?.(next);\n      },\n      [controlled, onOpenChange],\n    );\n    const baseId = useId();\n    const triggerId = triggerIdProp ?? `${baseId}-trigger`;\n    const contentId = contentIdProp ?? `${baseId}-content`;\n\n    return (\n      <CollapsibleContext.Provider\n        value={{ open, setOpen, disabled, triggerId, contentId }}\n      >\n        <div\n          ref={ref}\n          data-state={open ? \"open\" : \"closed\"}\n          data-disabled={disabled ? \"\" : undefined}\n          className={cn(className)}\n          {...rest}\n        >\n          {children}\n        </div>\n      </CollapsibleContext.Provider>\n    );\n  },\n);\n\ntype ClonableChild = ReactElement<{\n  onClick?: (event: MouseEvent<HTMLElement>) => void;\n  [key: string]: unknown;\n}>;\n\nexport interface CollapsibleTriggerProps\n  extends ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Merge trigger behaviour onto the single child element instead of rendering a `<button>`. */\n  asChild?: boolean;\n}\n\n/** Toggles the nearest `Collapsible`. Unstyled `<button>` by default. */\nexport const CollapsibleTrigger = forwardRef<\n  HTMLButtonElement,\n  CollapsibleTriggerProps\n>(function CollapsibleTrigger(\n  { asChild, children, onClick, className, disabled: disabledProp, ...rest },\n  ref,\n) {\n  const context = useCollapsible(\"CollapsibleTrigger\");\n  const disabled = context.disabled || Boolean(disabledProp);\n  const shared = {\n    id: context.triggerId,\n    \"aria-expanded\": context.open,\n    \"aria-controls\": context.contentId,\n    \"data-state\": context.open ? \"open\" : \"closed\",\n    \"data-disabled\": disabled ? \"\" : undefined,\n  };\n\n  if (asChild && isValidElement(children)) {\n    const child = children as ClonableChild;\n    return cloneElement(child, {\n      ...shared,\n      onClick: (event: MouseEvent<HTMLElement>) => {\n        child.props.onClick?.(event);\n        if (!event.defaultPrevented && !disabled) context.setOpen(!context.open);\n      },\n    });\n  }\n\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      disabled={disabled}\n      className={cn(className)}\n      {...rest}\n      {...shared}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.setOpen(!context.open);\n      }}\n    >\n      {children}\n    </button>\n  );\n});\n\nexport interface CollapsibleContentProps extends HTMLAttributes<HTMLDivElement> {\n  /** Class for the inner wrapper that holds the children (padding goes here,\n   * never on the animated outer element, so the closed height is truly 0). */\n  innerClassName?: string;\n  /** Set to give the panel `role=\"region\"` labelled by its trigger. */\n  region?: boolean;\n  children?: ReactNode;\n}\n\n/** The disclosed panel. Stays mounted (inert while closed) so `aria-controls` always resolves. */\nexport function CollapsibleContent({\n  className,\n  innerClassName,\n  region = false,\n  children,\n  style,\n  ...rest\n}: CollapsibleContentProps) {\n  const { open, triggerId, contentId } = useCollapsible(\"CollapsibleContent\");\n  const reduce = useReducedMotion();\n  const fade = open ? OPEN_TRANSITION : CLOSE_TRANSITION;\n\n  return (\n    // Height is animated on purpose: disclosure is a layout job — the panel\n    // has to push the content below it down in flow, which transforms can't.\n    // Reduced motion snaps the height and keeps only the opacity fade.\n    <motion.div\n      {...(rest as Record<string, unknown>)}\n      id={contentId}\n      role={region ? \"region\" : undefined}\n      aria-labelledby={region ? triggerId : undefined}\n      data-state={open ? \"open\" : \"closed\"}\n      inert={!open}\n      initial={false}\n      animate={{ height: open ? \"auto\" : 0, opacity: open ? 1 : 0 }}\n      transition={\n        reduce ? { height: { duration: 0 }, opacity: { duration: 0.15 } } : fade\n      }\n      style={style}\n      className={cn(\"overflow-hidden\", className)}\n    >\n      <div className={innerClassName}>{children}</div>\n    </motion.div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\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"}]}