{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"accordion","type":"registry:component","title":"Accordion","description":"Topics that open in place, one at a time or several: FAQs and settings sections; each header is a button (with an optional fact at its end), and the panel's height follows its content and can be reversed mid-way.","categories":["layout"],"dependencies":["@radix-ui/react-accordion@1.2.20","@radix-ui/react-direction@1.1.4","clsx@^2.1.1","lucide-react@^1.48.0","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/accordion/accordion.module.css","type":"registry:file","target":"src/ui/beamline/components/accordion/accordion.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* The accordion is a size container: a narrow room tightens the header and folds the meta away; a wide one keeps\n     the answers at a reading measure instead of running them across the whole width. It never collapses when left\n     content-sized. */\n  .root {\n    container-type: inline-size;\n    min-width: min(16rem, 100%);\n  }\n  .item {\n    border-bottom: 1px solid var(--pui-line-soft);\n  }\n  .item:last-child {\n    border-bottom: 0;\n  }\n\n  /* A header row: the title in ink, an optional meta, a chevron that turns when open and brightens on hover (hover\n     arrives at once and leaves over --pui-motion-out); disabled headers use the system's disabled opacity. */\n  .trigger {\n    display: flex;\n    width: 100%;\n    align-items: center;\n    gap: var(--pui-space-3);\n    padding-block: calc(var(--pui-space-1) * 3.5);\n    border-radius: var(--pui-radius-chip);\n    color: var(--pui-ink);\n    font-size: var(--pui-text-base);\n    font-weight: var(--pui-weight-medium);\n    text-align: start;\n  }\n  .root[data-size=\"lg\"] .trigger {\n    padding-block: var(--pui-space-5);\n    font-size: var(--pui-text-lg);\n  }\n  .trigger:disabled {\n    cursor: not-allowed;\n    opacity: var(--pui-opacity-disabled);\n  }\n  .title {\n    flex: 1;\n    min-width: 0;\n  }\n  .meta {\n    flex: none;\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-regular);\n    font-variant-numeric: tabular-nums;\n  }\n  .chevron {\n    flex: none;\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n    color: var(--pui-ink-3);\n    transition:\n      rotate var(--pui-duration-standard) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .trigger:hover:not(:disabled) .chevron {\n    color: var(--pui-ink);\n    transition-duration: var(--pui-duration-standard), 0ms;\n  }\n  .trigger[data-state=\"open\"] .chevron {\n    rotate: 180deg;\n  }\n\n  /* The panel: a one-row grid going 0fr → 1fr, so the height animates to the content's own without measuring and a\n     reversal mid-way turns back from where it is (a keyframe would restart). It opens over --pui-duration-standard and\n     closes over --pui-motion-out; the words fade in a beat behind the height and leave first. Closed, the panel is\n     unseen, so its links are out of the Tab order and the screen reader. (The transition sits on this inner part: the\n     primitive measures its own node with transitions switched off.) */\n  .panel {\n    display: grid;\n    grid-template-rows: 0fr;\n    visibility: hidden;\n    transition:\n      grid-template-rows var(--pui-motion-out) var(--pui-ease-default),\n      visibility 0s linear var(--pui-motion-out);\n  }\n  [data-state=\"open\"] > .panel {\n    grid-template-rows: 1fr;\n    visibility: visible;\n    transition:\n      grid-template-rows var(--pui-duration-standard) var(--pui-ease-default),\n      visibility 0s;\n  }\n  .clip {\n    min-height: 0;\n    overflow: hidden;\n  }\n  .body {\n    padding-bottom: var(--pui-space-4);\n    padding-inline-end: var(--pui-space-8);\n    color: var(--pui-ink-2);\n    font-size: var(--pui-text-sm);\n    line-height: 1.6;\n    opacity: 0;\n    transition: opacity var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root[data-size=\"lg\"] .body {\n    padding-bottom: var(--pui-space-5);\n    font-size: var(--pui-text-base);\n  }\n  [data-state=\"open\"] > .panel .body {\n    opacity: 1;\n    transition: opacity var(--pui-duration-standard) var(--pui-ease-default) var(--pui-duration-fast);\n  }\n\n  /* narrow: a tighter header, the meta folds away, the answer takes the full width */\n  @container (width < 24rem) {\n    .meta {\n      display: none;\n    }\n    .body {\n      padding-inline-end: 0;\n    }\n  }\n  @container (width < 20rem) {\n    .trigger,\n    .root[data-size=\"lg\"] .trigger {\n      padding-block: var(--pui-space-3);\n      font-size: var(--pui-text-sm);\n    }\n  }\n  /* wide: paragraphs keep a reading measure instead of running across the whole width (a form or table inside keeps\n     the room) */\n  @container (width >= 42rem) {\n    .body > p {\n      max-width: var(--pui-measure);\n    }\n  }\n}\n"},{"path":"beamline/components/accordion/accordion.tsx","type":"registry:file","target":"src/ui/beamline/components/accordion/accordion.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { AccordionDefaults } from \"./defaults\";\nexport type { AccordionDefaults } from \"./defaults\";\n\nimport { useState, type CSSProperties, type Ref, type ReactNode } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport * as RadixAccordion from \"@radix-ui/react-accordion\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport styles from \"./accordion.module.css\";\n\nexport interface AccordionItem {\n  /** Stable key; the index is used when absent. */\n  id?: string;\n  /** Heading displayed for this record. Required. */\n  title: ReactNode;\n  /** A short fact at the header's end (\"3 issues\", \"Updated 2 d ago\"); it folds away in a narrow room. */\n  meta?: ReactNode;\n  /** Content displayed when the section is open. Required. */\n  content: ReactNode;\n  /** Whether this option is unavailable for selection. Omitted by default. */\n  disabled?: boolean;\n}\n\nexport interface AccordionClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the item part; no override by default. */\n  item?: string;\n  /** Classes for the header part; no override by default. */\n  header?: string;\n  /** Classes for the trigger part; no override by default. */\n  trigger?: string;\n  /** Classes for the meta part; no override by default. */\n  meta?: string;\n  /** Classes for the chevron part; no override by default. */\n  chevron?: string;\n  /** Classes for the content part; no override by default. */\n  content?: string;\n  /** Classes for the body part; no override by default. */\n  body?: string;\n}\n\nexport interface AccordionProps {\n  /** Items rendered in the component. Required. */\n  items: AccordionItem[];\n  /** Controlled open item index, or indices with multiple; null closes the single panel. Omitted by default. */\n  open?: number | number[] | null;\n  /** Initial open item index or indices; defaults to no open panels. */\n  defaultOpen?: number | number[];\n  /** Reports the open index or null in single mode, or indices in multiple mode; no callback by default. */\n  onOpenChange?: (open: number | number[] | null) => void;\n  /** Let several sections stay open. Default false: opening one closes the other. */\n  multiple?: boolean;\n  /** md for settings and side panels, lg for FAQ pages. */\n  size?: \"md\" | \"lg\";\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: AccordionClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n}\n\n/** Topics that open one at a time in place: FAQs, settings sections. Each header is a button; ↑ ↓ move between\n *  them, Home and End jump. A panel opens to its content's height and can be reversed mid-way (the height turns from\n *  where it is); a panel's content mounts when it first opens and stays, so closing it again is a glide, not a cut. */\nexport function Accordion({\n  items,\n  open: openProp,\n  defaultOpen,\n  onOpenChange,\n  multiple = false,\n  size: sizeProp,\n  className,\n  classNames,\n  style,\n  ref,\n}: AccordionProps) {\n  const scopeDefaults = useComponentDefaults<AccordionDefaults>(\"accordion\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const keyOf = (i: number) => items[i]?.id ?? String(i);\n  // Radix's previous uncontrolled state kept item ids, so a reordered list must keep the same panels open.\n  // held as item keys, so a reordered list keeps the same panels open; reported as indices (the prop's shape)\n  const [keys, setKeys] = useControllable(\n    openProp === undefined\n      ? undefined\n      : (openProp === null ? [] : Array.isArray(openProp) ? openProp : [openProp]).map(keyOf),\n    () => (defaultOpen === undefined ? [] : Array.isArray(defaultOpen) ? defaultOpen : [defaultOpen]).map(keyOf),\n  );\n  const active = multiple ? keys : keys.slice(0, 1);\n  // Keep content mounted after either a user action or an external controlled update opens it.\n  const [opened, setOpened] = useState(() => new Set(active));\n  if (active.some((key) => !opened.has(key))) setOpened(new Set([...opened, ...active]));\n  const change = (next: string | string[]) => {\n    const chosen = Array.isArray(next) ? next : next ? [next] : [];\n    const indices = chosen.map((key) => items.findIndex((_, i) => keyOf(i) === key)).filter((i) => i >= 0);\n    const nextOpen = multiple ? indices : (indices[0] ?? null);\n    setKeys(chosen);\n    onOpenChange?.(nextOpen);\n  };\n  const rows = items.map((item, i) => (\n    <RadixAccordion.Item\n      key={keyOf(i)}\n      value={keyOf(i)}\n      disabled={item.disabled}\n      data-slot=\"accordion-item\"\n      className={cn(styles.item, classNames?.item)}\n    >\n      <RadixAccordion.Header asChild>\n        <h3 data-slot=\"accordion-header\" className={classNames?.header}>\n          <RadixAccordion.Trigger data-slot=\"accordion-trigger\" className={cn(styles.trigger, classNames?.trigger)}>\n            <span data-slot=\"accordion-title\" className={styles.title}>\n              {item.title}\n            </span>\n            {item.meta != null && (\n              <span data-slot=\"accordion-meta\" className={cn(styles.meta, classNames?.meta)}>\n                {item.meta}\n              </span>\n            )}\n            <ChevronDown\n              aria-hidden\n              data-slot=\"accordion-chevron\"\n              className={cn(styles.chevron, classNames?.chevron)}\n            />\n          </RadixAccordion.Trigger>\n        </h3>\n      </RadixAccordion.Header>\n      <RadixAccordion.Content forceMount data-slot=\"accordion-content\" className={classNames?.content}>\n        <div className={styles.panel}>\n          <div className={styles.clip}>\n            <div data-slot=\"accordion-body\" className={cn(styles.body, classNames?.body)}>\n              {opened.has(keyOf(i)) || active.includes(keyOf(i)) ? item.content : null}\n            </div>\n          </div>\n        </div>\n      </RadixAccordion.Content>\n    </RadixAccordion.Item>\n  ));\n  const root = cn(styles.root, classNames?.root, className);\n  return multiple ? (\n    <RadixAccordion.Root\n      type=\"multiple\"\n      value={active}\n      onValueChange={change}\n      data-slot=\"accordion\"\n      data-size={size}\n      className={root}\n      style={style}\n      ref={ref}\n    >\n      {rows}\n    </RadixAccordion.Root>\n  ) : (\n    <RadixAccordion.Root\n      type=\"single\"\n      collapsible\n      value={active[0] ?? \"\"}\n      onValueChange={change}\n      data-slot=\"accordion\"\n      data-size={size}\n      className={root}\n      style={style}\n      ref={ref}\n    >\n      {rows}\n    </RadixAccordion.Root>\n  );\n}\n\nexport default Accordion;\n"},{"path":"beamline/components/accordion/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/accordion/defaults.ts","content":"/** Pure allowed presentation defaults for Accordion; no component or runtime imports. */\nexport interface AccordionDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"md\" | \"lg\";\n}\n"},{"path":"beamline/hooks/use-controllable.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-controllable.ts","content":"\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\n/**\n * One value in both forms (rules/CONVENTIONS.md): the prop while it is passed, otherwise the element's own copy that\n * starts at `initial` (a value, or a function that makes it once). The setter takes a value or an updater; an updater\n * reads the latest value even twice in one event, and every change reaches `onChange`, controlled or not.\n * `Next` is what a change can carry when it is narrower than the state: a range that starts empty (null) but is only\n * ever changed to a range.\n */\nexport function useControllable<T, Next extends T = T>(\n  value: T | undefined,\n  initial: T | (() => T),\n  onChange?: (next: Next) => void,\n) {\n  const [own, setOwn] = useState<T>(initial);\n  const current = value !== undefined ? value : own;\n  const latest = useRef(current);\n  latest.current = current;\n  const set = useCallback(\n    (next: Next | ((was: T) => Next)) => {\n      const resolved = typeof next === \"function\" ? (next as (was: T) => Next)(latest.current) : next;\n      latest.current = resolved;\n      if (value === undefined) setOwn(resolved);\n      onChange?.(resolved);\n    },\n    [value, onChange],\n  );\n  return [current, set] as const;\n}\n"},{"path":"beamline/lib/utils.ts","type":"registry:file","target":"src/ui/beamline/lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n/* The type roles (utilities.css) and the 2xs step are sizes, not text colours: without this, tailwind-merge reads\n   `text-overline` as a colour and drops it next to `text-ink-3`. A role and a size step still replace each other.\n   Likewise `font-title` is the look's title weight, not a family: read as a family it dropped `font-display` beside it,\n   and every merged page title lost the look's display face. */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-weight\": [{ font: [\"title\"] }],\n      \"font-family\": [{ font: [\"display\", \"numeric\", \"figure\"] }],\n      \"font-size\": [\n        {\n          text: [\n            \"2xs\",\n            \"display\",\n            \"title\",\n            \"heading\",\n            \"subheading\",\n            \"body\",\n            \"body-sm\",\n            \"reading\",\n            \"label\",\n            \"caption\",\n            \"overline\",\n            \"numeric\",\n            \"code\",\n          ],\n        },\n      ],\n    },\n  },\n});\n\n/** Merge class names, letting later Tailwind utilities win over earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/** A small seeded random generator (LCG): the same seed gives the same sequence, for stable sample data and jitter. */\nexport function rng(seed = 1) {\n  let s = seed >>> 0 || 1;\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n"}],"docs":"Accordion is in src/ui/beamline/components/accordion/accordion.tsx. The source is in src/ui/beamline (ui/beamline in a project without src/). After Tailwind, import its styles/beamline.css once, then styles/look-prism.css in your app stylesheet (use relative paths). Keep the shared base before all chosen looks. Wrap your app in <UIRoot preset={prism}> with UIRoot from its runtime/appearance and prism from its looks/prism.  shadcn replaces files when asked; use the Beamline managed-source CLI from the start for three-way upgrades that preserve edits.","meta":{"free":true,"version":"0.2.0-rc.42","library":"https://beamline.io/components/accordion"}}