{
  "name": "accordion",
  "type": "registry:ui",
  "description": "Accordion with collapsible items, single or multiple open",
  "files": [
    {
      "path": "ui/accordion.tsx",
      "type": "registry:ui",
      "content": "import { fv } from \"@facet-ui/react-variants\";\nimport { Accordion as AccordionPrimitive, type AccordionType } from \"@lattice-ui/react-accordion\";\nimport {\n  getPassthroughProps,\n  type PassthroughProps,\n  React,\n  toSlotProps,\n  useControllableState,\n} from \"@lattice-ui/react-runtime\";\nimport { TextSlot } from \"~/lib/text\";\nimport { type ClassName, cn } from \"~/lib/utils\";\n\n/**\n * The open values are mirrored here with `useControllableState` — the same\n * hook the primitive uses — because Lattice keeps its context private and the\n * trigger's chevron flips with its item. The primitive is then driven\n * controlled, so there is exactly one copy of the state; the item hands its\n * own `open` down a second context so the trigger does not need to know the\n * item's value.\n *\n * One recipe object rather than seven exports: every exported name costs a\n * Luau register once Vela inlines its runtime. See\n * docs/decisions/luau-register-limit.md.\n */\nexport const accordionVariants = {\n  // The rule between items is `divide-y` on the root, not `border-b` on the\n  // item. A `border-*` class lowers to a `UIStroke`, which outlines the whole\n  // instance — Roblox has no per-side stroke, so Vela drops `border-b` and the\n  // surviving `border-border` would box every item. `divide-y` interleaves real\n  // one-pixel frames between children, and leaves no rule under the last item.\n  root: fv(\"flex-col w-full h-fit divide-y divide-border\"),\n  item: fv(\"flex-col w-full h-fit\"),\n  // `items-start` is shadcn's, and it is the one that matters when the heading\n  // wraps: the chevron stays level with the first line rather than drifting to\n  // the middle of the block.\n  trigger: fv(\"flex-row items-start justify-between w-full h-fit gap-4 rounded-md py-4\"),\n  triggerLabel: fv(\"text-left text-sm font-medium text-foreground\"),\n  chevron: fv(\"size-fit text-xs font-normal text-muted-foreground text-center\"),\n  content: fv(\"flex-col gap-2 w-full h-fit overflow-hidden pb-4\"),\n  contentText: fv(\"w-full h-fit whitespace-normal text-left text-sm font-normal text-muted-foreground\"),\n};\n\nconst AccordionContext = React.createContext<{ isOpen: (value: string) => boolean } | undefined>(undefined);\nconst AccordionItemContext = React.createContext<{ open: boolean } | undefined>(undefined);\n\nexport type AccordionProps = {\n  /** `single` closes the previous item when the next opens; `multiple` lets them accumulate. */\n  type?: AccordionType;\n  value?: string | string[];\n  defaultValue?: string | string[];\n  onValueChange?: (value: string | string[]) => void;\n  /** In `single` mode, whether the open item can be clicked closed again. */\n  collapsible?: boolean;\n  className?: ClassName;\n  children?: React.ReactNode;\n};\n\nexport type AccordionItemProps = {\n  value: string;\n  disabled?: boolean;\n  className?: ClassName;\n  children?: React.ReactNode;\n} & PassthroughProps<Frame>;\n\nexport type AccordionTriggerProps = {\n  Text?: string;\n  className?: ClassName;\n  children?: React.ReactNode;\n};\n\nexport type AccordionContentProps = {\n  Text?: string;\n  className?: ClassName;\n  children?: React.ReactNode;\n} & PassthroughProps<Frame>;\n\nconst ITEM_OWN_PROPS = [\"value\", \"disabled\", \"className\", \"children\"] as const;\nconst CONTENT_OWN_PROPS = [\"Text\", \"className\", \"children\"] as const;\n\nexport function Accordion(props: AccordionProps) {\n  const [value, setValue] = useControllableState<string | string[] | undefined>({\n    value: props.value,\n    defaultValue: props.defaultValue,\n    onChange: props.onValueChange as (value: string | string[] | undefined) => void,\n  });\n\n  const isOpen = React.useCallback(\n    (itemValue: string) => {\n      if (typeIs(value, \"table\")) {\n        return (value as string[]).includes(itemValue);\n      }\n      return value === itemValue;\n    },\n    [value],\n  );\n\n  const contextValue = React.useMemo(() => ({ isOpen }), [isOpen]);\n\n  return (\n    <AccordionPrimitive.Root\n      collapsible={props.collapsible}\n      onValueChange={setValue}\n      type={props.type}\n      value={value ?? (props.type === \"multiple\" ? [] : \"\")}\n    >\n      <AccordionContext.Provider value={contextValue}>\n        <frame className={cn(accordionVariants.root({ className: props.className }))} BackgroundTransparency={1}>\n          {props.children}\n        </frame>\n      </AccordionContext.Provider>\n    </AccordionPrimitive.Root>\n  );\n}\n\nexport function AccordionItem(props: AccordionItemProps) {\n  const accordion = React.useContext(AccordionContext);\n  if (accordion === undefined) {\n    error(\"[AccordionItem] must be rendered inside an Accordion.\");\n  }\n\n  const open = accordion.isOpen(props.value);\n  const itemContextValue = React.useMemo(() => ({ open }), [open]);\n\n  return (\n    <AccordionItemContext.Provider value={itemContextValue}>\n      <AccordionPrimitive.Item\n        className={cn(accordionVariants.item({ className: props.className }))}\n        disabled={props.disabled}\n        value={props.value}\n        {...toSlotProps(getPassthroughProps<Frame>(props, ITEM_OWN_PROPS))}\n      >\n        {props.children}\n      </AccordionPrimitive.Item>\n    </AccordionItemContext.Provider>\n  );\n}\n\nexport function AccordionTrigger(props: AccordionTriggerProps) {\n  const item = React.useContext(AccordionItemContext);\n  if (item === undefined) {\n    error(\"[AccordionTrigger] must be rendered inside an AccordionItem.\");\n  }\n\n  // No layout class on the header: `flex-*` would lower to a `uilistlayout`\n  // sibling next to its single child, and the primitive types `children` as\n  // one element.\n  return (\n    <AccordionPrimitive.Header className=\"w-full h-fit\">\n      <AccordionPrimitive.Trigger className={cn(accordionVariants.trigger({ className: props.className }))}>\n        {/* biome-ignore lint/complexity/noUselessFragments: the primitive types\n            `children` as a single element (what `asChild` merges onto), so the\n            label and the chevron have to arrive as one. */}\n        <>\n          <TextSlot Text={props.Text} className={cn(accordionVariants.triggerLabel())}>\n            {props.children}\n          </TextSlot>\n          {/* An icon font does not exist on Roblox, so the chevron is a text\n              glyph, flipped by rotation — replace it to use your own artwork. */}\n          <textlabel\n            className={cn(accordionVariants.chevron())}\n            Rotation={item.open ? 180 : 0}\n            Text=\"▾\"\n            BackgroundTransparency={1}\n            BorderSizePixel={0}\n          />\n        </>\n      </AccordionPrimitive.Trigger>\n    </AccordionPrimitive.Header>\n  );\n}\n\nexport function AccordionContent(props: AccordionContentProps) {\n  return (\n    <AccordionPrimitive.Content\n      className={cn(accordionVariants.content({ className: props.className }))}\n      {...toSlotProps(getPassthroughProps<Frame>(props, CONTENT_OWN_PROPS))}\n    >\n      <TextSlot Text={props.Text} className={cn(accordionVariants.contentText())}>\n        {props.children}\n      </TextSlot>\n    </AccordionPrimitive.Content>\n  );\n}\n"
    }
  ],
  "registryDependencies": [
    "utils",
    "text"
  ],
  "dependencies": [
    "@facet-ui/react-variants@^0.1.1",
    "@lattice-ui/react-runtime@^0.8.0",
    "@lattice-ui/react-accordion@^0.8.0"
  ],
  "tokens": [
    "border",
    "foreground",
    "muted-foreground"
  ]
}
