{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"collapsible","type":"registry:ui","title":"Collapsible","description":"A single disclosure: a trigger that expands and collapses a region, animated without measuring so an interrupted toggle simply reverses, with the closed content kept in the page.","categories":["navigation"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/collapsible/Collapsible.tsx","content":"\"use client\";\n\nimport { forwardRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useFancyId } from \"../../internals/use-id\";\nimport { COLLAPSIBLE_KEY } from \"./types\";\nimport type { CollapsibleContext } from \"./types\";\n\ninterface CollapsibleOwnProps {\n\t/**\n\t * Whether the content is expanded. Passing it makes the state controlled, the\n\t * React counterpart of the Svelte source's `$bindable`; omit it to let the\n\t * root own its state and report every change through `onOpenChange`.\n\t */\n\topen?: boolean;\n\t/** The state an uncontrolled root starts in. Defaults to `false`. */\n\tdefaultOpen?: boolean;\n\t/** Called with the new state each time the trigger toggles it. */\n\tonOpenChange?: (open: boolean) => void;\n\t/** Makes the trigger inert. A controlled `open` can still be changed from outside. */\n\tdisabled?: boolean;\n\t/** A `CollapsibleTrigger` and a `CollapsibleContent`. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes. */\n\tclassName?: string;\n}\n\n/** Native `<div>` attributes and listeners are forwarded to the root. */\nexport interface CollapsibleProps\n\textends Omit<ComponentPropsWithoutRef<\"div\">, keyof CollapsibleOwnProps>, CollapsibleOwnProps {}\n\n/**\n * The root of the collapsible compound: it owns the open state and the id pair\n * its trigger and content are wired up with.\n *\n * The element arrives through the ref channel rather than a `ref` prop, per\n * PORTING.md; the Svelte source declares `ref = $bindable(null)`.\n */\nexport const Collapsible = forwardRef<HTMLDivElement, CollapsibleProps>(\n\t(\n\t\t{\n\t\t\topen: openProp,\n\t\t\tdefaultOpen = false,\n\t\t\tonOpenChange,\n\t\t\tdisabled = false,\n\t\t\tchildren,\n\t\t\tclassName,\n\t\t\t...rest\n\t\t},\n\t\tforwardedRef\n\t) => {\n\t\t// SSR-stable: `internals/use-id.js`'s `uid()` is client-only, and the\n\t\t// trigger/content ids must already agree on the first server-rendered\n\t\t// paint so `aria-controls` is correct before hydration.\n\t\tconst uid = useFancyId();\n\n\t\t// The Svelte source's `open` is `$bindable(false)`: a consumer can bind it\n\t\t// or leave it alone and let the component keep its own copy. React has no\n\t\t// such channel, so the prop is controlled when it is passed and this local\n\t\t// copy takes over when it is not. Either way `onOpenChange` fires.\n\t\tconst [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n\t\tconst isControlled = openProp !== undefined;\n\t\tconst open = isControlled ? openProp : uncontrolledOpen;\n\n\t\tconst context: CollapsibleContext = {\n\t\t\topen,\n\t\t\tdisabled,\n\t\t\t// The only place the state changes from inside.\n\t\t\ttoggle() {\n\t\t\t\tif (disabled) return;\n\t\t\t\tconst next = !open;\n\t\t\t\tif (!isControlled) setUncontrolledOpen(next);\n\t\t\t\tonOpenChange?.(next);\n\t\t\t},\n\t\t\ttriggerId: `${uid}-trigger`,\n\t\t\tcontentId: `${uid}-content`,\n\t\t};\n\n\t\treturn (\n\t\t\t<COLLAPSIBLE_KEY.Provider value={context}>\n\t\t\t\t<div\n\t\t\t\t\t{...rest}\n\t\t\t\t\tref={forwardedRef}\n\t\t\t\t\tclassName={cn(\"ft-collapsible\", className)}\n\t\t\t\t\tdata-state={open ? \"open\" : \"closed\"}\n\t\t\t\t\tdata-disabled={disabled ? \"\" : undefined}\n\t\t\t\t>\n\t\t\t\t\t{children}\n\t\t\t\t</div>\n\t\t\t</COLLAPSIBLE_KEY.Provider>\n\t\t);\n\t}\n);\nCollapsible.displayName = \"Collapsible\";\n","type":"registry:ui","target":"@ui/fancy/components/collapsible/Collapsible.tsx"},{"path":"react/src/components/collapsible/CollapsibleContent.tsx","content":"\"use client\";\n\nimport { forwardRef, useContext, useEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, TransitionEvent } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useInertAttribute } from \"../../internals/dom/use-inert-attribute\";\nimport { COLLAPSIBLE_KEY } from \"./types\";\nimport { isCollapseEnd, prefersReducedMotionNow, settleDelayMs } from \"./collapsible-core\";\n\ninterface CollapsibleContentOwnProps {\n\t/**\n\t * Keeps the children mounted while the content is closed. On by default: a\n\t * closed panel stays in the DOM (inert and hidden) so server-rendered\n\t * markup, find-in-page and form state survive. Pass `false` to unmount the\n\t * children once the collapse has finished and mount them again on open, for\n\t * content that should not run while closed (an iframe, a video).\n\t * Defaults to `true`, unlike `TabsContent`: the trigger's `aria-controls` always points at an element that exists, the closed markup is server-rendered, and nothing has to be measured.\n\t */\n\tforceMount?: boolean;\n\t/** The content. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes, merged onto the inner body so padding collapses with the panel. */\n\tclassName?: string;\n}\n\n/**\n * Native `<div>` attributes and listeners are forwarded to the region; `id`,\n * `data-state`, `inert` and `visibility` stay the component's.\n */\nexport interface CollapsibleContentProps\n\textends\n\t\tOmit<ComponentPropsWithoutRef<\"div\">, keyof CollapsibleContentOwnProps>,\n\t\tCollapsibleContentOwnProps {}\n\n/**\n * The region a `CollapsibleTrigger` expands. It animates `grid-template-rows`\n * between `0fr` and `1fr`, so nothing is measured and an interrupted toggle\n * simply reverses; once a close has finished it goes inert and hidden. The\n * element arrives through the ref channel rather than a `ref` prop, per\n * PORTING.md.\n */\nexport const CollapsibleContent = forwardRef<HTMLDivElement, CollapsibleContentProps>(\n\t({ forceMount = true, children, className, onTransitionEnd, style, ...rest }, forwardedRef) => {\n\t\t// Undefined outside a Collapsible: nothing is ever open, so the content\n\t\t// stays collapsed, the same graceful degradation as the trigger.\n\t\tconst context = useContext(COLLAPSIBLE_KEY);\n\t\tconst open = context?.open ?? false;\n\n\t\t// \"Closed and finished collapsing\". Starts true for a panel that is born\n\t\t// closed, so a server-rendered closed panel is already hidden. It is only\n\t\t// ever set from the settle path below and cleared the moment the panel\n\t\t// opens, which is what makes a double toggle safe: a close that gets\n\t\t// interrupted never reaches its own settle.\n\t\tconst [settled, setSettled] = useState(!open);\n\t\tconst settledRef = useRef(settled);\n\t\tconst hidden = settled && !open;\n\n\t\tconst nodeRef = useRef<HTMLDivElement | null>(null);\n\t\tconst inertRef = useInertAttribute<HTMLDivElement>(!open);\n\t\tconst setRef = useComposedRefs(forwardedRef, nodeRef, inertRef);\n\n\t\tconst timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n\t\tfunction clearTimer() {\n\t\t\tif (timerRef.current !== undefined) clearTimeout(timerRef.current);\n\t\t\ttimerRef.current = undefined;\n\t\t}\n\n\t\tfunction settle(value: boolean) {\n\t\t\tsettledRef.current = value;\n\t\t\tsetSettled(value);\n\t\t}\n\n\t\t// Reacts to `open` only. A close settles on `transitionend`; the deadline\n\t\t// is the net for the cases where that event never comes (reduced motion,\n\t\t// no declared duration, a panel inside a hidden ancestor).\n\t\tuseEffect(() => {\n\t\t\tclearTimer();\n\t\t\tif (open) {\n\t\t\t\tif (settledRef.current) settle(false);\n\t\t\t\treturn clearTimer;\n\t\t\t}\n\t\t\tif (settledRef.current) return clearTimer;\n\t\t\tconst node = nodeRef.current;\n\t\t\tconst delay = settleDelayMs(node ? getComputedStyle(node) : null, prefersReducedMotionNow());\n\t\t\tif (delay === 0) {\n\t\t\t\tsettle(true);\n\t\t\t\treturn clearTimer;\n\t\t\t}\n\t\t\ttimerRef.current = setTimeout(() => {\n\t\t\t\ttimerRef.current = undefined;\n\t\t\t\tsettle(true);\n\t\t\t}, delay);\n\t\t\treturn clearTimer;\n\t\t}, [open]);\n\n\t\tfunction handleTransitionEnd(event: TransitionEvent<HTMLDivElement>) {\n\t\t\tif (isCollapseEnd(event) && !open) {\n\t\t\t\tclearTimer();\n\t\t\t\tsettle(true);\n\t\t\t}\n\t\t\tonTransitionEnd?.(event);\n\t\t}\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\t{...rest}\n\t\t\t\tref={setRef}\n\t\t\t\tid={context?.contentId ?? rest.id}\n\t\t\t\tclassName=\"ft-collapsible-content grid grid-rows-[0fr] transition-[grid-template-rows] duration-[var(--ft-collapsible-duration,240ms)] ease-[cubic-bezier(0.16,1,0.3,1)] data-[state=open]:grid-rows-[1fr] motion-reduce:transition-none\"\n\t\t\t\tdata-state={open ? \"open\" : \"closed\"}\n\t\t\t\tstyle={hidden ? { ...style, visibility: \"hidden\" } : style}\n\t\t\t\tonTransitionEnd={handleTransitionEnd}\n\t\t\t>\n\t\t\t\t<div className=\"min-h-0 overflow-hidden\">\n\t\t\t\t\t<div className={cn(\"ft-collapsible-body\", className)}>\n\t\t\t\t\t\t{forceMount || !hidden ? children : null}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t}\n);\nCollapsibleContent.displayName = \"CollapsibleContent\";\n","type":"registry:ui","target":"@ui/fancy/components/collapsible/CollapsibleContent.tsx"},{"path":"react/src/components/collapsible/CollapsibleTrigger.tsx","content":"\"use client\";\n\nimport { forwardRef, useContext } from \"react\";\nimport type { ComponentPropsWithoutRef, MouseEvent, ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { COLLAPSIBLE_KEY } from \"./types\";\n\ninterface CollapsibleTriggerOwnProps {\n\t/** Disables this trigger on top of the root's own `disabled`. */\n\tdisabled?: boolean;\n\t/** The trigger's content. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes. */\n\tclassName?: string;\n}\n\n/**\n * Native `<button>` attributes and listeners are forwarded; `id`, `type`,\n * `aria-expanded`, `aria-controls` and `data-state` stay the component's.\n */\nexport interface CollapsibleTriggerProps\n\textends\n\t\tOmit<ComponentPropsWithoutRef<\"button\">, keyof CollapsibleTriggerOwnProps>,\n\t\tCollapsibleTriggerOwnProps {}\n\n/**\n * The button that toggles a `Collapsible`. The element arrives through the ref\n * channel rather than a `ref` prop, per PORTING.md.\n */\nexport const CollapsibleTrigger = forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(\n\t({ disabled = false, children, className, onClick, ...rest }, forwardedRef) => {\n\t\t// Undefined outside a Collapsible: the trigger then has nothing to toggle\n\t\t// and renders as a plain button rather than throwing.\n\t\tconst context = useContext(COLLAPSIBLE_KEY);\n\n\t\tconst isDisabled = disabled || (context?.disabled ?? false);\n\t\tconst isOpen = context?.open ?? false;\n\n\t\t// The native `disabled` attribute is the real gate, but a synthetic click\n\t\t// fired straight at the element walks past it, so the handler repeats the\n\t\t// guard. A guarded click reaches neither the toggle nor the consumer.\n\t\tfunction handleClick(event: MouseEvent<HTMLButtonElement>) {\n\t\t\tif (isDisabled) return;\n\t\t\tcontext?.toggle();\n\t\t\tonClick?.(event);\n\t\t}\n\n\t\treturn (\n\t\t\t<button\n\t\t\t\t{...rest}\n\t\t\t\tref={forwardedRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\tid={context?.triggerId ?? rest.id}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"ft-collapsible-trigger cursor-pointer\",\n\t\t\t\t\t\"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none\",\n\t\t\t\t\t\"disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\tdisabled={isDisabled}\n\t\t\t\taria-expanded={context ? isOpen : rest[\"aria-expanded\"]}\n\t\t\t\taria-controls={context?.contentId ?? rest[\"aria-controls\"]}\n\t\t\t\tdata-state={context ? (isOpen ? \"open\" : \"closed\") : undefined}\n\t\t\t\tdata-disabled={isDisabled ? \"\" : undefined}\n\t\t\t\tonClick={handleClick}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</button>\n\t\t);\n\t}\n);\nCollapsibleTrigger.displayName = \"CollapsibleTrigger\";\n","type":"registry:ui","target":"@ui/fancy/components/collapsible/CollapsibleTrigger.tsx"},{"path":"react/src/components/collapsible/collapsible-core.ts","content":"\"use client\";\n\n/**\n * The framework-free half of the collapse motion, shared byte for byte by the\n * Svelte, React and Vue packages.\n *\n * `CollapsibleContent` animates `grid-template-rows` between `0fr` and `1fr`\n * and has to know when that transition is over, to take the panel out of the\n * tab order and the accessibility tree. `transitionend` is the signal, but it\n * never arrives when the transition did not run: reduced motion, a stylesheet\n * that declares no duration, a panel inside a hidden ancestor. These helpers\n * turn the element's own computed transition into a deadline so the panel\n * settles in every one of those cases, and never earlier than its animation.\n */\n\n/** The media query that switches the motion off. */\nexport const REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\";\n\n/**\n * Milliseconds assumed when the element declares no transition timing at all\n * (a computed style that is empty, as in a test DOM with no stylesheet).\n */\nexport const DEFAULT_SETTLE_MS = 300;\n\n/** Slack added on top of a declared transition so `transitionend` wins the race. */\nexport const SETTLE_SLACK_MS = 60;\n\n/** The `transitionend` property name of the collapse. */\nexport const COLLAPSE_PROPERTY = \"grid-template-rows\";\n\n/** One-shot read of the user's motion preference; `false` where `matchMedia` is missing. */\nexport function prefersReducedMotionNow(): boolean {\n\tif (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return false;\n\treturn window.matchMedia(REDUCED_MOTION_QUERY).matches;\n}\n\n/** `\"0.2s, 150ms\"` to `[200, 150]`; a token that is not a time becomes `NaN`. */\nexport function parseTimeList(value: string): number[] {\n\treturn value\n\t\t.split(\",\")\n\t\t.map((token) => token.trim())\n\t\t.filter((token) => token !== \"\")\n\t\t.map((token) => {\n\t\t\tconst match = /^(-?\\d*\\.?\\d+)(ms|s)$/.exec(token);\n\t\t\tif (!match) return Number.NaN;\n\t\t\treturn Number.parseFloat(match[1] ?? \"\") * (match[2] === \"s\" ? 1000 : 1);\n\t\t});\n}\n\n/** The slice of a computed style the deadline reads. */\nexport interface TransitionTiming {\n\ttransitionDuration: string;\n\ttransitionDelay: string;\n}\n\n/**\n * How long to wait for `transitionend` before settling the panel anyway.\n *\n * `0` means \"settle now\": the user asked for reduced motion, or the element's\n * longest declared transition lasts no time at all, so no event will come.\n * Anything else is the declared duration plus its delay plus a little slack;\n * an element that declares nothing waits `DEFAULT_SETTLE_MS`.\n */\nexport function settleDelayMs(timing: TransitionTiming | null, reduced: boolean): number {\n\tif (reduced) return 0;\n\tconst durations = timing ? parseTimeList(timing.transitionDuration).filter(Number.isFinite) : [];\n\tif (durations.length === 0) return DEFAULT_SETTLE_MS + SETTLE_SLACK_MS;\n\tconst delays = parseTimeList(timing?.transitionDelay ?? \"\").filter(Number.isFinite);\n\tconst total = Math.max(\n\t\t...durations.map(\n\t\t\t(duration, index) => duration + (delays[index % Math.max(delays.length, 1)] ?? 0)\n\t\t)\n\t);\n\treturn total <= 0 ? 0 : total + SETTLE_SLACK_MS;\n}\n\n/** Whether a `transitionend` is the collapse of this very panel, not a child's or another property's. */\nexport function isCollapseEnd(event: {\n\ttarget: unknown;\n\tcurrentTarget: unknown;\n\tpropertyName: string;\n}): boolean {\n\treturn event.target === event.currentTarget && event.propertyName === COLLAPSE_PROPERTY;\n}\n","type":"registry:ui","target":"@ui/fancy/components/collapsible/collapsible-core.ts"},{"path":"react/src/components/collapsible/index.ts","content":"\"use client\";\n\nexport { Collapsible } from \"./Collapsible\";\nexport type { CollapsibleProps } from \"./Collapsible\";\nexport { CollapsibleTrigger } from \"./CollapsibleTrigger\";\nexport type { CollapsibleTriggerProps } from \"./CollapsibleTrigger\";\nexport { CollapsibleContent } from \"./CollapsibleContent\";\nexport type { CollapsibleContentProps } from \"./CollapsibleContent\";\nexport { COLLAPSIBLE_KEY } from \"./types\";\nexport type { CollapsibleContext } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/collapsible/index.ts"},{"path":"react/src/components/collapsible/types.ts","content":"\"use client\";\n\nimport { createContext } from \"react\";\n\n/**\n * The contract between a collapsible root and its trigger and content.\n *\n * `Collapsible` owns the open state; `CollapsibleTrigger` and\n * `CollapsibleContent` read it through context instead of a prop and a\n * callback threaded down by hand. `AccordionItem` publishes the same context,\n * so an accordion item's trigger and region are a collapsible's, driven by the\n * accordion's value instead of a flag of their own.\n */\n\n/** What a root publishes. The pieces read it; only the root (or an accordion item) writes it. */\nexport interface CollapsibleContext {\n\t/** Whether the content is expanded. */\n\treadonly open: boolean;\n\t/** Whether the trigger is inert. Does not stop a controlled change from opening it. */\n\treadonly disabled: boolean;\n\t/** Flips the open state and reports it. A no-op while disabled. */\n\ttoggle(): void;\n\t/** The id the trigger renders; the content's `aria-labelledby` target when it is a region. */\n\treadonly triggerId: string;\n\t/** The id the content renders; the trigger's `aria-controls` target. */\n\treadonly contentId: string;\n}\n\n/**\n * The context a trigger/content reads to find its surrounding root. The Svelte\n * source publishes it under a `Symbol` context key; React's own context object\n * plays that role here, so the exported name is kept and the value is a\n * `React.Context` rather than a symbol. Read it as optional: a piece rendered\n * outside a root gets `undefined` rather than throwing.\n */\nexport const COLLAPSIBLE_KEY = createContext<CollapsibleContext | undefined>(undefined);\nCOLLAPSIBLE_KEY.displayName = \"CollapsibleContext\";\n","type":"registry:ui","target":"@ui/fancy/components/collapsible/types.ts"}]}