{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "collapsible",
  "type": "registry:ui",
  "title": "Collapsible",
  "description": "One section that folds away, plus a Show 3 more list that cascades in.",
  "categories": [
    "disclosure"
  ],
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json"
  ],
  "files": [
    {
      "path": "components/ui/collapsible.tsx",
      "content": "\"use client\";\nimport { Collapsible as BaseCollapsible } from \"@base-ui/react/collapsible\";\nimport { Children } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport type CollapsibleProps = Omit<BaseCollapsible.Root.Props, \"className\"> & { className?: string };\n\n/** Open state lives here: `open` + `onOpenChange`, or `defaultOpen`. */\nexport function Collapsible({ className, ...rest }: CollapsibleProps) {\n  return <BaseCollapsible.Root className={cn(\"flex min-w-0 flex-col\", className)} {...rest} />;\n}\n\nexport type CollapsibleTriggerProps = Omit<BaseCollapsible.Trigger.Props, \"className\" | \"children\"> & {\n  children: React.ReactNode;\n  /** A section header row, or a quiet text button (\"Show 3 more\"). Both lead with the chevron, so a label that changes never moves it. */\n  variant?: \"row\" | \"inline\";\n  /** Label shown while open (\"Show less\"). Both labels share one box, so the button never changes width. */\n  openLabel?: React.ReactNode;\n  /** Trailing text in a row trigger: a count, a status. */\n  hint?: React.ReactNode;\n  className?: string;\n};\n\nexport function CollapsibleTrigger({ children, variant = \"row\", openLabel, hint, className, ...rest }: CollapsibleTriggerProps) {\n  const row = variant === \"row\";\n  return (\n    <BaseCollapsible.Trigger\n      data-variant={variant}\n      className={cn(\n        \"group/trigger relative flex min-w-0 select-none items-center text-left\",\n        \"touch-manipulation [-webkit-tap-highlight-color:transparent]\",\n        \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-fg-3\",\n        \"transition-[background-color,color,scale] duration-150 ease-out active:duration-75\",\n        \"data-disabled:cursor-not-allowed data-disabled:text-fg-4\",\n        row\n          ? \"-mx-2 min-h-9 gap-1.5 rounded-lg px-2 text-[13px] font-medium tracking-[-0.005em] text-fg hover:not-data-disabled:bg-hover active:not-data-disabled:bg-fg/[0.06]\"\n          : cn(\n              \"-mx-2 h-8 w-fit gap-1.5 rounded-md px-2 text-[12.5px] font-medium text-fg-2 hover:not-data-disabled:bg-hover hover:not-data-disabled:text-fg active:not-data-disabled:scale-[0.97]\",\n              // Draws 32px tall; on touch it lends itself 6px above and below.\n              \"pointer-coarse:before:absolute pointer-coarse:before:-inset-y-1.5 pointer-coarse:before:inset-x-0 pointer-coarse:before:content-['']\",\n            ),\n        className,\n      )}\n      {...rest}\n    >\n      {row && (\n        <span className=\"-ml-0.5 grid size-4 shrink-0 place-items-center text-fg-3 transition-colors duration-150 group-hover/trigger:text-fg-2 group-data-[panel-open]/trigger:text-fg-2\">\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth={1.4}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            aria-hidden\n            className=\"transition-[rotate] duration-[240ms] ease-in-out-quart motion-reduce:transition-none group-data-[panel-open]/trigger:rotate-90\"\n          >\n            <path d=\"m6.25 4.5 3.5 3.5-3.5 3.5\" />\n          </svg>\n        </span>\n      )}\n\n      {!row && (\n        <svg\n          width=\"14\"\n          height=\"14\"\n          viewBox=\"0 0 16 16\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={1.5}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          aria-hidden\n          className=\"shrink-0 text-fg-3 transition-[rotate,color] duration-[240ms] ease-in-out-quart motion-reduce:transition-none group-hover/trigger:text-fg-2 group-data-[panel-open]/trigger:rotate-180\"\n        >\n          <path d=\"m4.5 6.25 3.5 3.5 3.5-3.5\" />\n        </svg>\n      )}\n\n      <Label open={openLabel}>{children}</Label>\n\n      {row && hint != null && <span className=\"ml-auto shrink-0 pl-3 text-[12px] font-normal tabular text-fg-3\">{hint}</span>}\n    </BaseCollapsible.Trigger>\n  );\n}\n\n/**\n * Both labels sit in one grid cell. The outgoing one rises and fades, the incoming\n * one arrives from below; `visibility` follows, so only the current label is read out.\n */\nfunction Label({ children, open }: { children: React.ReactNode; open?: React.ReactNode }) {\n  if (open == null) return <span className=\"min-w-0 truncate\">{children}</span>;\n  const swap = \"col-start-1 row-start-1 whitespace-nowrap transition-[opacity,translate,visibility] duration-200 ease-out-expo motion-reduce:translate-y-0\";\n  return (\n    <span className=\"grid min-w-0 overflow-hidden py-0.5\">\n      <span className={cn(swap, \"group-data-[panel-open]/trigger:invisible group-data-[panel-open]/trigger:-translate-y-2 group-data-[panel-open]/trigger:opacity-0\")}>\n        {children}\n      </span>\n      <span className={cn(swap, \"invisible translate-y-2 opacity-0 group-data-[panel-open]/trigger:visible group-data-[panel-open]/trigger:translate-y-0 group-data-[panel-open]/trigger:opacity-100\")}>\n        {open}\n      </span>\n    </span>\n  );\n}\n\nexport type CollapsiblePanelProps = Omit<BaseCollapsible.Panel.Props, \"className\"> & {\n  className?: string;\n  /** Classes for the inner box that fades in once the height has room for it. */\n  contentClassName?: string;\n};\n\n/** Height follows `--collapsible-panel-height`; the content fades up 50ms behind it. */\nexport function CollapsiblePanel({ className, contentClassName, children, ...rest }: CollapsiblePanelProps) {\n  return (\n    <BaseCollapsible.Panel\n      className={cn(\n        \"group/panel h-(--collapsible-panel-height) overflow-hidden\",\n        \"transition-[height] duration-[260ms] ease-out-quart data-ending-style:duration-200 data-ending-style:ease-in-out-quart\",\n        \"data-starting-style:h-0 data-ending-style:h-0 motion-reduce:transition-none\",\n        className,\n      )}\n      {...rest}\n    >\n      <div className={cn(fadeIn, contentClassName)}>{children}</div>\n    </BaseCollapsible.Panel>\n  );\n}\n\nconst fadeIn = cn(\n  \"transition-[opacity,translate] delay-[50ms] duration-[280ms] ease-out-expo\",\n  \"group-data-[starting-style]/panel:-translate-y-1 group-data-[starting-style]/panel:opacity-0\",\n  \"group-data-[ending-style]/panel:opacity-0 group-data-[ending-style]/panel:delay-0 group-data-[ending-style]/panel:duration-[140ms]\",\n  \"motion-reduce:translate-y-0 motion-reduce:delay-0\",\n);\n\n// Revealed rows arrive 20ms apart, capped at the eighth, and leave together.\nconst stagger = cn(\n  \"[&>*]:transition-[opacity,translate] [&>*]:duration-[280ms] [&>*]:ease-out-expo [&>*]:delay-[40ms]\",\n  \"[&>*:nth-child(2)]:delay-[60ms] [&>*:nth-child(3)]:delay-[80ms] [&>*:nth-child(4)]:delay-[100ms] [&>*:nth-child(5)]:delay-[120ms]\",\n  \"[&>*:nth-child(6)]:delay-[140ms] [&>*:nth-child(7)]:delay-[160ms] [&>*:nth-child(n+8)]:delay-[180ms]\",\n  \"group-data-[starting-style]/more:[&>*]:-translate-y-1.5 group-data-[starting-style]/more:[&>*]:opacity-0\",\n  \"group-data-[ending-style]/more:[&>*]:opacity-0 group-data-[ending-style]/more:[&>*]:delay-0! group-data-[ending-style]/more:[&>*]:duration-[140ms]\",\n  \"motion-reduce:[&>*]:translate-y-0! motion-reduce:[&>*]:delay-0!\",\n);\n\nexport type CollapsibleListProps = Omit<CollapsibleProps, \"children\"> & {\n  children: React.ReactNode;\n  /** How many children stay visible while collapsed. */\n  limit?: number;\n  /** Closed label. Receives how many are hidden. */\n  moreLabel?: (hidden: number) => React.ReactNode;\n  lessLabel?: React.ReactNode;\n  /** Classes for both groups of rows, so they read as one list (gap, dividers). */\n  listClassName?: string;\n  triggerClassName?: string;\n};\n\n/**\n * The \"Show 3 more\" pattern. With `limit` or fewer children it renders them plainly,\n * with no button: a control that reveals nothing is a lie.\n */\nexport function CollapsibleList({\n  children,\n  limit = 3,\n  moreLabel = (n) => (\n    <>\n      Show <span className=\"tabular\">{n}</span> more\n    </>\n  ),\n  lessLabel = \"Show less\",\n  listClassName,\n  triggerClassName,\n  className,\n  ...rest\n}: CollapsibleListProps) {\n  const items = Children.toArray(children);\n  const head = items.slice(0, limit);\n  const tail = items.slice(limit);\n  const list = cn(\"flex flex-col\", listClassName);\n\n  if (!tail.length) return <div className={cn(list, className)}>{head}</div>;\n\n  return (\n    <Collapsible className={className} {...rest}>\n      <div className={list}>{head}</div>\n      <BaseCollapsible.Panel\n        className={cn(\n          \"group/more h-(--collapsible-panel-height) overflow-hidden\",\n          \"transition-[height] duration-[260ms] ease-out-quart data-ending-style:duration-200 data-ending-style:ease-in-out-quart\",\n          \"data-starting-style:h-0 data-ending-style:h-0 motion-reduce:transition-none\",\n        )}\n      >\n        <div className={cn(list, stagger)}>{tail}</div>\n      </BaseCollapsible.Panel>\n      <CollapsibleTrigger variant=\"inline\" openLabel={lessLabel} className={cn(\"mt-1\", triggerClassName)}>\n        {moreLabel(tail.length)}\n      </CollapsibleTrigger>\n    </Collapsible>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/collapsible"
}
