{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "collapsible", "type": "registry:block", "title": "Collapsible", "description": "A compound collapsible primitive with Inline, Card, Section, Sidebar, and Preview variants, animated height/opacity, and full keyboard/ARIA support.", "author": "Wensity ", "dependencies": [ "@base-ui/react", "@tabler/icons-react", "clsx", "tailwind-merge" ], "registryDependencies": [], "files": [ { "path": "registry/wensity/lib/utils.ts", "type": "registry:lib", "target": "@lib/utils.ts", "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n" }, { "path": "registry/wensity/collapsible.tsx", "type": "registry:component", "target": "@components/wensity/collapsible.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Collapsible as BaseCollapsible } from \"@base-ui/react/collapsible\";\nimport { IconChevronDown, IconChevronRight } from \"@tabler/icons-react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ─── Types ─────────────────────────────────────────────────── */\n\nexport type CollapsibleVariant = \"inline\" | \"card\" | \"section\" | \"sidebar\" | \"preview\";\n\nexport interface CollapsibleProps extends Omit, \"children\"> {\n variant?: CollapsibleVariant;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n disabled?: boolean;\n /** preview variant only: visible height while collapsed, in px. Default 96. */\n previewHeight?: number;\n children: React.ReactNode;\n}\n\nexport interface CollapsibleTriggerProps\n extends Omit, \"children\"> {\n children?: React.ReactNode | ((state: { open: boolean }) => React.ReactNode);\n /** Hide the chevron indicator (e.g. when the trigger already communicates state, like preview's \"Show more\"). */\n hideIcon?: boolean;\n}\n\nexport interface CollapsibleContentProps extends React.HTMLAttributes {}\n\n/* ─── Context ───────────────────────────────────────────────── */\n\ntype CollapsibleContextValue = {\n variant: CollapsibleVariant;\n previewHeight: number;\n open: boolean;\n};\n\nconst CollapsibleContext = React.createContext(null);\n\nfunction useCollapsibleContext() {\n const ctx = React.useContext(CollapsibleContext);\n if (!ctx) throw new Error(\"Collapsible compound components must be used within \");\n return ctx;\n}\n\n/* ─── Variant Styles ────────────────────────────────────────── */\n\nconst rootVariantClasses: Record = {\n inline: \"\",\n card: cn(\n \"overflow-hidden rounded-[var(--primitive-radius-surface)] border\",\n \"border-[var(--border)] bg-[var(--primitive-surface-elevated)]\",\n \"[box-shadow:var(--primitive-shadow-raised)]\",\n ),\n section: cn(\"border-t first:border-t-0\", \"border-[var(--primitive-border-subtle)]\"),\n sidebar: \"\",\n preview: \"relative flex flex-col gap-2\",\n};\n\nconst triggerVariantClasses: Record = {\n inline: \"gap-2 text-[14px] font-medium\",\n card: \"gap-3 px-4 py-3.5 text-[15px] font-medium\",\n section: \"gap-3 py-5 text-left\",\n sidebar: cn(\n \"gap-2 rounded-full px-3 py-2 text-[14px] font-medium\",\n \"data-[panel-open]:bg-[var(--primitive-surface-selected)]\",\n ),\n preview: \"gap-1.5 text-[13px] font-medium\",\n};\n\nconst triggerHoverClasses: Record = {\n inline: \"\",\n card: \"hover:bg-[var(--primitive-surface-hover)]\",\n section: \"\",\n sidebar: \"hover:bg-[var(--primitive-surface-hover)] data-[panel-open]:hover:bg-[var(--primitive-surface-selected)]\",\n preview: \"\",\n};\n\nconst contentPaddingClasses: Record = {\n inline: \"pt-2\",\n card: \"px-4 pb-4\",\n section: \"pb-5\",\n sidebar: \"py-1 pl-6\",\n preview: \"\",\n};\n\nconst panelMotionClass = cn(\n \"h-[var(--collapsible-panel-height)] overflow-hidden\",\n \"transition-[height,opacity] duration-300 ease-[var(--primitive-ease,cubic-bezier(0.23,1,0.32,1))]\",\n \"data-[starting-style]:h-0 data-[starting-style]:opacity-0\",\n \"data-[ending-style]:h-0 data-[ending-style]:opacity-0 data-[ending-style]:duration-200\",\n \"motion-reduce:transition-none motion-reduce:data-[starting-style]:opacity-100 motion-reduce:data-[ending-style]:opacity-100\",\n);\n\n/* ─── ─────────────────────────────────────────── */\n\nexport const Collapsible = React.forwardRef(\n (\n {\n variant = \"inline\",\n open: controlledOpen,\n defaultOpen = false,\n onOpenChange,\n disabled = false,\n previewHeight = 96,\n className,\n children,\n style,\n ...props\n },\n ref,\n ) => {\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : uncontrolledOpen;\n\n const handleOpenChange = React.useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n onOpenChange?.(next);\n },\n [isControlled, onOpenChange],\n );\n\n const ctx = React.useMemo(\n () => ({ variant, previewHeight, open }),\n [variant, previewHeight, open],\n );\n\n const rootStyle =\n variant === \"preview\"\n ? ({\n ...style,\n \"--wensity-collapsible-preview-height\": `${previewHeight}px`,\n } as React.CSSProperties)\n : style;\n\n return (\n \n handleOpenChange(next)}\n disabled={disabled}\n data-slot=\"collapsible\"\n data-variant={variant}\n className={cn(rootVariantClasses[variant], className)}\n style={rootStyle}\n {...props}\n >\n {children}\n \n \n );\n },\n);\nCollapsible.displayName = \"Collapsible\";\n\n/* ─── ──────────────────────────────────── */\n\nexport const CollapsibleTrigger = React.forwardRef(\n ({ className, children, hideIcon = false, ...props }, ref) => {\n const { variant, open } = useCollapsibleContext();\n\n return (\n \n {variant === \"sidebar\" && !hideIcon ? (\n \n \n \n ) : null}\n \n {typeof children === \"function\" ? children({ open }) : children}\n \n {variant !== \"sidebar\" && !hideIcon ? (\n \n \n \n ) : null}\n \n );\n },\n);\nCollapsibleTrigger.displayName = \"CollapsibleTrigger\";\n\n/* ─── ──────────────────────────────────── */\n\nexport const CollapsibleContent = React.forwardRef(\n ({ className, children, style, ...props }, ref) => {\n const { variant, previewHeight, open } = useCollapsibleContext();\n const isPreview = variant === \"preview\";\n const innerRef = React.useRef(null);\n const [overflows, setOverflows] = React.useState(false);\n const [expandedHeight, setExpandedHeight] = React.useState(null);\n\n React.useLayoutEffect(() => {\n if (!isPreview || !innerRef.current) return;\n const el = innerRef.current;\n const measure = () => {\n const nextHeight = el.scrollHeight;\n setExpandedHeight(nextHeight);\n setOverflows(nextHeight > previewHeight + 1);\n };\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(el);\n return () => observer.disconnect();\n }, [isPreview, previewHeight, children]);\n\n if (isPreview) {\n const previewStyle = {\n ...style,\n // Drive peek height ourselves — Base UI's closed height is 0 / hidden,\n // which breaks the \"show more\" preview contract.\n height: open ? (expandedHeight ?? \"auto\") : previewHeight,\n overflow: \"hidden\",\n } as React.CSSProperties;\n\n return (\n