{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"collapsible","type":"registry:component","title":"Collapsible","description":"Unstyled disclosure primitive (Collapsible, CollapsibleTrigger with asChild, CollapsibleContent) with controlled or uncontrolled open state, an in-flow height morph that fades content in and closes faster than it opens, aria-expanded/aria-controls wiring, an inert closed panel and a reduced-motion snap.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/collapsible.tsx","type":"registry:component","target":"@components/motion/collapsible.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/motion/collapsible\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"}]}