{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"collapsible","type":"registry:component","title":"Animated Collapsible","description":"Expandable content with a smooth reveal.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/collapsible.tsx","type":"registry:component","target":"@components/motion/collapsible.tsx","content":"\"use client\";\n// beui.dev/components/motion/collapsible\n\nimport { ChevronDown } from \"lucide-react\";\nimport { type HTMLMotionProps, isValidMotionProp, motion, useReducedMotion } from \"motion/react\";\nimport {\n  cloneElement,\n  createContext,\n  type ReactElement,\n  type Ref,\n  type RefObject,\n  useCallback,\n  useContext,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type CollapsibleState = {\n  open: boolean;\n  disabled: boolean;\n  setOpen: (open: boolean) => void;\n  toggle: () => void;\n};\n\ntype CollapsibleContextValue = CollapsibleState & {\n  reduce: boolean;\n  contentId: string;\n  triggerId: string;\n  triggerRef: RefObject<HTMLButtonElement | null>;\n};\n\nconst CollapsibleContext = createContext<CollapsibleContextValue | null>(null);\n\nfunction useCollapsibleContext(component: string) {\n  const context = useContext(CollapsibleContext);\n  if (!context) throw new Error(`${component} must be used within <Collapsible>`);\n  return context;\n}\n\n/** Shared state and actions for custom children inside a Collapsible root. */\nexport function useCollapsible(): CollapsibleState {\n  return useCollapsibleContext(\"useCollapsible\");\n}\n\nfunction mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    const cleanups = refs.map((ref) => {\n      if (typeof ref === \"function\") return ref(node);\n      if (ref) ref.current = node;\n      return undefined;\n    });\n    if (!node) return;\n    return () => {\n      refs.forEach((ref, index) => {\n        const cleanup = cleanups[index];\n        if (typeof cleanup === \"function\") cleanup();\n        else if (typeof ref === \"function\") ref(null);\n        else if (ref) ref.current = null;\n      });\n    };\n  };\n}\n\nexport interface CollapsibleProps extends Omit<HTMLMotionProps<\"div\">, \"layout\"> {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  disabled?: boolean;\n  /** Layout classes for the inner wrapper, separate from the animated surface. */\n  contentClassName?: string;\n}\n\nexport function Collapsible({\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n  disabled = false,\n  className,\n  contentClassName,\n  children,\n  style,\n  ...props\n}: CollapsibleProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const open = controlledOpen ?? internalOpen;\n  const reduce = useReducedMotion() ?? false;\n  const id = useId();\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const stateRef = useRef({ open, controlled: controlledOpen !== undefined, disabled, onOpenChange });\n  useLayoutEffect(() => {\n    stateRef.current = { open, controlled: controlledOpen !== undefined, disabled, onOpenChange };\n  }, [controlledOpen, disabled, onOpenChange, open]);\n  const setOpen = useCallback((next: boolean) => {\n    const state = stateRef.current;\n    if (state.disabled || next === state.open) return;\n    if (!state.controlled) {\n      stateRef.current = { ...state, open: next };\n      setInternalOpen(next);\n    }\n    state.onOpenChange?.(next);\n  }, []);\n  const toggle = useCallback(() => setOpen(!stateRef.current.open), [setOpen]);\n  const context = useMemo<CollapsibleContextValue>(\n    () => ({ open, disabled, reduce, contentId: `${id}-content`, triggerId: `${id}-trigger`, triggerRef, setOpen, toggle }),\n    [disabled, id, open, reduce, setOpen, toggle],\n  );\n\n  return (\n    <CollapsibleContext.Provider value={context}>\n      <motion.div\n        {...props}\n        layout={!reduce}\n        transition={{ layout: SPRING_LAYOUT }}\n        data-slot=\"collapsible\"\n        data-state={open ? \"open\" : \"closed\"}\n        data-disabled={disabled || undefined}\n        className={cn(\"relative w-full\", className)}\n        style={{ ...style, originY: 0 }}\n      >\n        <motion.div\n          layout={reduce ? false : \"position\"}\n          transition={{ layout: SPRING_LAYOUT }}\n          style={{ originY: 0 }}\n          className={cn(\"min-w-0\", contentClassName)}\n        >\n          {children}\n        </motion.div>\n      </motion.div>\n    </CollapsibleContext.Provider>\n  );\n}\n\nexport interface CollapsibleTriggerProps extends Omit<HTMLMotionProps<\"button\">, \"id\" | \"layout\"> {\n  /** Reuse a button element or a component that forwards its props and ref to a button. */\n  render?: ReactElement;\n}\n\nexport function CollapsibleTrigger({\n  className,\n  children,\n  onClick,\n  disabled,\n  ref,\n  render,\n  style,\n  ...props\n}: CollapsibleTriggerProps) {\n  const context = useCollapsibleContext(\"CollapsibleTrigger\");\n  const child = render as ReactElement<HTMLMotionProps<\"button\">> | undefined;\n  const childRef = child?.props.ref;\n  const mergedRef = useMemo(\n    () => mergeRefs(context.triggerRef, ref, childRef),\n    [context.triggerRef, ref, childRef],\n  );\n  const unavailable = context.disabled || disabled || child?.props.disabled;\n  const handleClick: NonNullable<HTMLMotionProps<\"button\">[\"onClick\"]> = (event) => {\n    child?.props.onClick?.(event);\n    onClick?.(event);\n    if (!event.defaultPrevented && !unavailable) context.toggle();\n  };\n  const triggerProps = {\n    id: context.triggerId,\n    type: \"button\" as const,\n    disabled: unavailable,\n    \"aria-expanded\": context.open,\n    \"aria-controls\": context.contentId,\n    \"data-slot\": \"collapsible-trigger\",\n    \"data-state\": context.open ? \"open\" : \"closed\",\n    \"data-disabled\": unavailable || undefined,\n    onClick: handleClick,\n    ref: mergedRef,\n  };\n\n  if (child) {\n    if (typeof child.type === \"string\" && child.type !== \"button\") {\n      throw new Error(\"CollapsibleTrigger render must be a button or a component that renders a button.\");\n    }\n    const forwarded = typeof child.type === \"string\"\n      ? Object.fromEntries(Object.entries(props).filter(([key]) => !isValidMotionProp(key)))\n      : props;\n    return cloneElement(child, {\n      ...forwarded,\n      ...triggerProps,\n      className: cn(className, child.props.className),\n      style: { ...style, ...child.props.style },\n      children: children === undefined ? child.props.children : children,\n    });\n  }\n\n  return (\n    <motion.button\n      {...props}\n      {...triggerProps}\n      style={style}\n      layout={context.reduce ? false : \"position\"}\n      transition={{ layout: SPRING_LAYOUT }}\n      className={cn(\n        \"flex min-h-10 w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-sm font-medium outline-none transition-colors hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n        className,\n      )}\n    >\n      {children}\n    </motion.button>\n  );\n}\n\nexport interface CollapsibleContentProps extends Omit<HTMLMotionProps<\"div\">, \"id\" | \"layout\" | \"animate\" | \"initial\"> {\n  /** Padding and layout classes for the measured content, inside the clipping wrapper. */\n  contentClassName?: string;\n}\n\nexport function CollapsibleContent({\n  className,\n  contentClassName,\n  children,\n  ref,\n  style,\n  ...props\n}: CollapsibleContentProps) {\n  const context = useCollapsibleContext(\"CollapsibleContent\");\n  const contentRef = useRef<HTMLDivElement>(null);\n  const innerRef = useRef<HTMLDivElement>(null);\n  const [height, setHeight] = useState(0);\n  const mergedRef = useMemo(() => mergeRefs(contentRef, ref), [ref]);\n\n  useLayoutEffect(() => {\n    const node = innerRef.current;\n    if (!node) return;\n    const measure = () => setHeight(node.offsetHeight);\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  useLayoutEffect(() => {\n    if (!context.open && contentRef.current?.contains(document.activeElement)) {\n      context.triggerRef.current?.focus({ preventScroll: true });\n    }\n  }, [context.open, context.triggerRef]);\n\n  return (\n    <motion.div\n      {...props}\n      ref={mergedRef}\n      id={context.contentId}\n      role=\"region\"\n      aria-labelledby={context.triggerId}\n      aria-hidden={!context.open}\n      inert={!context.open}\n      data-slot=\"collapsible-content\"\n      data-state={context.open ? \"open\" : \"closed\"}\n      initial={false}\n      layout={!context.reduce}\n      transition={{ layout: SPRING_LAYOUT }}\n      // A one-pixel closed box keeps transform projection continuous at zero.\n      // Geometry changes once; the inner layout projection keeps text sharp.\n      style={{ ...style, height: context.open ? height || \"auto\" : 1, originY: 0 }}\n      className={cn(\"overflow-hidden\", className)}\n    >\n      <motion.div\n        ref={innerRef}\n        layout={!context.reduce}\n        style={{ originY: 0 }}\n        initial={false}\n        animate={{ opacity: context.open ? 1 : 0 }}\n        transition={{ opacity: { duration: context.open ? 0.18 : 0.12, ease: EASE_OUT }, layout: SPRING_LAYOUT }}\n        className={cn(\"flow-root\", contentClassName)}\n      >\n        {children}\n      </motion.div>\n    </motion.div>\n  );\n}\n\nexport interface CollapsibleIndicatorProps extends HTMLMotionProps<\"span\"> {}\n\nexport function CollapsibleIndicator({ className, children, ...props }: CollapsibleIndicatorProps) {\n  const { open, reduce } = useCollapsibleContext(\"CollapsibleIndicator\");\n  return (\n    <motion.span\n      {...props}\n      aria-hidden=\"true\"\n      data-slot=\"collapsible-indicator\"\n      initial={false}\n      animate={{ transform: `rotate(${open ? 180 : 0}deg)` }}\n      transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n      className={cn(\"ml-auto inline-flex shrink-0 items-center justify-center text-muted-foreground\", className)}\n    >\n      {children ?? <ChevronDown className=\"size-4\" />}\n    </motion.span>\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"}]}