{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "unfold-list",
  "type": "registry:ui",
  "title": "Unfold List",
  "description": "A disclosure list whose panels turn down onto the page from their top edge.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/loomui/unfold-list.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface UnfoldState {\n  open: string[]\n  toggle: (value: string) => void\n  duration: number\n}\n\nconst UnfoldContext = React.createContext<UnfoldState | null>(null)\n\nfunction useUnfold() {\n  const context = React.useContext(UnfoldContext)\n  if (!context) {\n    throw new Error(\"UnfoldItem must be rendered inside an UnfoldList.\")\n  }\n  return context\n}\n\nexport interface UnfoldListProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"defaultValue\" | \"onChange\"\n> {\n  /** One panel open at a time, or as many as the reader wants. */\n  type?: \"single\" | \"multiple\"\n  /** Panels open before anyone has touched the list. */\n  defaultValue?: string | string[]\n  /** Open panels, if you are holding the state yourself. */\n  value?: string | string[]\n  /** Called with the open panels whenever they change. */\n  onValueChange?: (value: string[]) => void\n  /** In `single` mode, allow the open panel to be closed again. */\n  collapsible?: boolean\n  /** Milliseconds one panel takes to unfold. */\n  duration?: number\n}\n\nconst toArray = (value: string | string[] | undefined) =>\n  value === undefined ? [] : Array.isArray(value) ? value : [value]\n\nexport function UnfoldList({\n  children,\n  className,\n  type = \"single\",\n  defaultValue,\n  value,\n  onValueChange,\n  collapsible = true,\n  duration = 320,\n  ...props\n}: UnfoldListProps) {\n  const [ownOpen, setOwnOpen] = React.useState(() => toArray(defaultValue))\n  const isControlled = value !== undefined\n  const open = isControlled ? toArray(value) : ownOpen\n\n  const toggle = React.useCallback(\n    (item: string) => {\n      const isOpen = open.includes(item)\n      const next =\n        type === \"single\"\n          ? isOpen\n            ? collapsible\n              ? []\n              : open\n            : [item]\n          : isOpen\n            ? open.filter((entry) => entry !== item)\n            : [...open, item]\n\n      if (!isControlled) {\n        setOwnOpen(next)\n      }\n      onValueChange?.(next)\n    },\n    [open, type, collapsible, isControlled, onValueChange]\n  )\n\n  const state = React.useMemo(\n    () => ({ open, toggle, duration }),\n    [open, toggle, duration]\n  )\n\n  return (\n    <UnfoldContext.Provider value={state}>\n      <div\n        data-slot=\"unfold-list\"\n        className={cn(\n          \"divide-border border-border divide-y border-y\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </UnfoldContext.Provider>\n  )\n}\n\nexport interface UnfoldItemProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"title\"\n> {\n  /** Identifies this panel to the list. Must be unique within it. */\n  value: string\n  /** The row itself, which is the button that opens the panel. */\n  title: React.ReactNode\n}\n\n/**\n * The panel is a grid row animated from `0fr` to `1fr`, so it opens to whatever\n * height its content happens to be without anything having to measure it first\n * and without a fixed height to keep in sync.\n */\nexport function UnfoldItem({\n  value,\n  title,\n  children,\n  className,\n  ...props\n}: UnfoldItemProps) {\n  const { open, toggle, duration } = useUnfold()\n  const isOpen = open.includes(value)\n  const id = React.useId()\n\n  return (\n    <div\n      data-slot=\"unfold-item\"\n      data-state={isOpen ? \"open\" : \"closed\"}\n      className={cn(\"group\", className)}\n      style={{ \"--unfold-duration\": `${duration}ms` } as React.CSSProperties}\n      {...props}\n    >\n      <h3 className=\"m-0\">\n        <button\n          type=\"button\"\n          id={`${id}-trigger`}\n          aria-expanded={isOpen}\n          aria-controls={`${id}-panel`}\n          onClick={() => toggle(value)}\n          className=\"text-foreground focus-visible:ring-ring/50 flex w-full items-center justify-between gap-4 py-4 text-left text-sm font-medium outline-none focus-visible:ring-2\"\n        >\n          {title}\n          <span\n            aria-hidden=\"true\"\n            className=\"border-muted-foreground/60 size-2 shrink-0 rotate-45 border-r border-b transition-[rotate,border-color] duration-[var(--unfold-duration)] ease-[var(--ease-out-quart)] group-data-[state=open]:-rotate-135 motion-reduce:transition-none\"\n          />\n        </button>\n      </h3>\n\n      <div\n        id={`${id}-panel`}\n        role=\"region\"\n        aria-labelledby={`${id}-trigger`}\n        // Kept in the document so the fold can animate both ways. `inert`\n        // takes a closed panel out of the tab order and off the a11y tree,\n        // which `hidden` would do at the cost of the closing animation.\n        inert={!isOpen}\n        className=\"grid grid-rows-[0fr] transition-[grid-template-rows] duration-[var(--unfold-duration)] ease-[var(--ease-out-quart)] group-data-[state=open]:grid-rows-[1fr] motion-reduce:transition-none\"\n      >\n        {/* The fold. The panel turns down onto the page from its top edge as\n            the row it sits in opens. */}\n        <div className=\"overflow-hidden [perspective:600px]\">\n          <div className=\"text-muted-foreground origin-top [rotate:x_-25deg] pb-4 text-sm opacity-0 transition-[opacity,rotate] duration-[var(--unfold-duration)] ease-[var(--ease-out-quart)] group-data-[state=open]:[rotate:x_0deg] group-data-[state=open]:opacity-100 motion-reduce:[rotate:none] motion-reduce:opacity-100 motion-reduce:transition-none\">\n            {children}\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "ease-out-quart": "cubic-bezier(0.165, 0.84, 0.44, 1)"
    }
  }
}