shadimage
corr
componentanimated-disclosuresource: ok · hit

Animated Disclosure

An expandable disclosure panel with vertical and horizontal motion directions.

npx shadcn add "https://ui.corr.sh/r/animated-disclosure.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
iconslucide-react
animationmotion

Registry dependencies

button

Files1 · source cached 10/8/2026, 5:17:07 PM

registry/corr/components/animated-disclosure.tsx
"use client"

import type { ReactNode } from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { ChevronDown } from "lucide-react"

import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"

export type AnimatedDisclosureDirection = "down" | "up" | "left" | "right"

export function AnimatedDisclosure({
  open,
  onOpenChange,
  title,
  children,
  direction = "down",
  trigger,
  disabled = false,
  className,
  triggerClassName,
  contentClassName,
}: {
  open: boolean
  onOpenChange: (open: boolean) => void
  title: ReactNode
  children: ReactNode
  direction?: AnimatedDisclosureDirection
  trigger?: ReactNode
  disabled?: boolean
  className?: string
  triggerClassName?: string
  contentClassName?: string
}) {
  const reduceMotion = useReducedMotion()
  const isHorizontal = direction === "left" || direction === "right"
  const label = typeof title === "string" ? title : "Toggle disclosure"

  if (isHorizontal) {
    return (
      <div
        className={cn(
          "inline-flex items-center gap-2",
          direction === "left" && "flex-row-reverse",
          className
        )}
      >
        <Button
          type="button"
          size="icon"
          aria-expanded={open}
          aria-label={label}
          disabled={disabled}
          onClick={() => onOpenChange(!open)}
          className={cn(
            "size-10 shrink-0 rounded-full shadow-sm",
            triggerClassName
          )}
        >
          {trigger ?? <ChevronDown className="size-4" />}
        </Button>
        <AnimatePresence initial={false}>
          {open ? (
            <motion.div
              initial={
                reduceMotion
                  ? { opacity: 0 }
                  : {
                      opacity: 0,
                      width: 0,
                      x: direction === "left" ? 6 : -6,
                    }
              }
              animate={
                reduceMotion
                  ? { opacity: 1 }
                  : { opacity: 1, width: "auto", x: 0 }
              }
              exit={
                reduceMotion
                  ? { opacity: 0 }
                  : {
                      opacity: 0,
                      width: 0,
                      x: direction === "left" ? 6 : -6,
                    }
              }
              transition={{
                duration: reduceMotion ? 0 : 0.24,
                ease: "easeOut",
              }}
              className="overflow-hidden"
            >
              <div
                className={cn(
                  "w-72 rounded-md border bg-card p-4 shadow-sm",
                  contentClassName
                )}
              >
                <div className="text-sm font-medium">{title}</div>
                <div className="mt-2 text-sm leading-6 text-muted-foreground">
                  {children}
                </div>
              </div>
            </motion.div>
          ) : null}
        </AnimatePresence>
      </div>
    )
  }

  const content = (
    <AnimatePresence initial={false}>
      {open ? (
        <motion.div
          initial={reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }}
          animate={
            reduceMotion ? { opacity: 1 } : { height: "auto", opacity: 1 }
          }
          exit={reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }}
          transition={{ duration: reduceMotion ? 0 : 0.22, ease: "easeOut" }}
          className="overflow-hidden"
        >
          <div
            className={cn(
              "px-4 py-3 text-sm leading-6 text-muted-foreground",
              direction === "up" ? "border-b" : "border-t",
              contentClassName
            )}
          >
            {children}
          </div>
        </motion.div>
      ) : null}
    </AnimatePresence>
  )

  return (
    <div className={cn("rounded-md border bg-card", className)}>
      {direction === "up" ? content : null}
      <Button
        type="button"
        variant="ghost"
        aria-expanded={open}
        disabled={disabled}
        onClick={() => onOpenChange(!open)}
        className={cn(
          "flex h-auto w-full justify-between rounded-none px-4 py-3 text-left",
          triggerClassName
        )}
      >
        <span className="font-medium">{title}</span>
        <motion.span
          animate={{
            rotate: direction === "up" ? (open ? 0 : 180) : open ? 180 : 0,
          }}
          transition={
            reduceMotion ? { duration: 0 } : { duration: 0.18, ease: "easeOut" }
          }
        >
          <ChevronDown className="size-4 text-muted-foreground" />
        </motion.span>
      </Button>
      {direction === "down" ? content : null}
    </div>
  )
}

More from corr

Browse all components in this library →