{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "px-accordion",
  "title": "Accordion",
  "description": "Accordion whose pixel plus signs fold into minus signs while the panels open smoothly; one or several items can stay open.",
  "registryDependencies": [
    "https://elements.lumesec.ai/r/lumesec-theme.json",
    "https://elements.lumesec.ai/r/lumesec-math.json",
    "https://elements.lumesec.ai/r/lumesec-motion.json",
    "https://elements.lumesec.ai/r/lumesec-pixel-canvas.json",
    "https://elements.lumesec.ai/r/lumesec-use-controllable-state.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/components/lumesec/px-accordion.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { clamp, mixRgb, rgba } from \"@/lib/lumesec/math\";\nimport { prefersReducedMotion } from \"@/lib/lumesec/motion\";\nimport { usePixelCanvas } from \"@/lib/lumesec/pixel-canvas\";\nimport { useControllableState } from \"@/lib/lumesec/use-controllable-state\";\nimport { cn } from \"@/lib/utils\";\n\n/** One section of the accordion. */\nexport interface PxAccordionItem {\n  /** Identifies the item in `value`. Defaults to `title`. */\n  value?: string;\n  /** Header text. */\n  title: string;\n  /** Muted text at the end of the header. */\n  summary?: string;\n  /** Panel content. */\n  content: React.ReactNode;\n}\n\n/** Props of `PxAccordion`. Other `div` props go to the root element. */\nexport interface PxAccordionProps extends Omit<React.ComponentProps<\"div\">, \"children\" | \"defaultValue\" | \"onChange\"> {\n  /** The sections, top to bottom. */\n  items: readonly PxAccordionItem[];\n  /** Lets several items stay open; otherwise opening one closes the others. */\n  multiple?: boolean;\n  /** Controlled list of open items (their `value`, or `title`). Use with `onValueChange`. */\n  value?: string[];\n  /** Items open initially when uncontrolled. */\n  defaultValue?: string[];\n  /** Called with the open items whenever the user opens or closes one. */\n  onValueChange?: (value: string[]) => void;\n}\n\nconst NO_ITEMS: string[] = [];\n\n/**\n * Accordion whose pixel plus signs fold into minus signs while the panels open smoothly.\n *\n * @example\n * <PxAccordion\n *   defaultValue={[\"What is Ultracode?\"]}\n *   items={[\n *     { title: \"What is Ultracode?\", content: \"A mode that sends xhigh effort.\" },\n *     { title: \"How is usage counted?\", content: \"Usage resets weekly.\" },\n *   ]}\n * />\n */\nexport function PxAccordion({\n  items,\n  multiple = false,\n  value: valueProp,\n  defaultValue = NO_ITEMS,\n  onValueChange,\n  className,\n  ...props\n}: PxAccordionProps) {\n  const baseId = React.useId();\n  const [open, setOpen] = useControllableState({ value: valueProp, defaultValue, onChange: onValueChange });\n  const headerRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n\n  const toggle = (itemValue: string) => {\n    setOpen((current) => {\n      if (current.includes(itemValue)) return current.filter((entry) => entry !== itemValue);\n      return multiple ? [...current, itemValue] : [itemValue];\n    });\n  };\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, index: number) => {\n    const target = headerTarget(event.key, index, items.length);\n    if (target === null) return;\n    event.preventDefault();\n    headerRefs.current[target]?.focus();\n  };\n\n  return (\n    <div data-slot=\"px-accordion\" className={cn(\"block text-foreground\", className)} {...props}>\n      {items.map((item, index) => {\n        const itemValue = item.value ?? item.title;\n        const isOpen = open.includes(itemValue);\n        const headerId = `${baseId}-header-${index}`;\n        const panelId = `${baseId}-panel-${index}`;\n        return (\n          <div\n            key={itemValue}\n            data-state={isOpen ? \"open\" : \"closed\"}\n            className=\"group border-b border-border first:border-t\"\n          >\n            <h3 className=\"m-0\">\n              <button\n                ref={(node) => {\n                  headerRefs.current[index] = node;\n                }}\n                id={headerId}\n                type=\"button\"\n                aria-expanded={isOpen}\n                aria-controls={panelId}\n                className=\"flex w-full cursor-pointer items-center gap-3 border-0 bg-transparent px-1 py-3.5 text-left text-[14px] leading-normal font-semibold outline-none select-none hover:text-lumesec focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lumesec focus-visible:outline-solid\"\n                onClick={() => toggle(itemValue)}\n                onKeyDown={(event) => onKeyDown(event, index)}\n              >\n                <PlusMinus open={isOpen} />\n                <span>{item.title}</span>\n                {item.summary && (\n                  <span className=\"ml-auto text-[12px] font-normal text-muted-foreground\">{item.summary}</span>\n                )}\n              </button>\n            </h3>\n            <div\n              id={panelId}\n              role=\"region\"\n              aria-labelledby={headerId}\n              inert={!isOpen}\n              className=\"grid grid-rows-[0fr] transition-[grid-template-rows] duration-[350ms] ease-[cubic-bezier(.2,.8,.2,1)] group-data-[state=open]:grid-rows-[1fr] motion-reduce:transition-none\"\n            >\n              <div className=\"overflow-hidden\">\n                <div className=\"relative mb-4 ml-1.5 -translate-y-1 pl-5 text-[13.5px] leading-[1.6] text-[color-mix(in_srgb,var(--foreground)_82%,var(--muted-foreground))] opacity-0 transition-[opacity,translate] duration-300 ease-[ease] group-data-[state=open]:translate-y-0 group-data-[state=open]:opacity-100 group-data-[state=open]:delay-[80ms] before:absolute before:top-1 before:bottom-1 before:left-0 before:w-0.5 before:bg-[image:radial-gradient(var(--lumesec)_1px,transparent_1.4px)] before:bg-[length:3px_4px] before:bg-repeat-y before:content-[''] motion-reduce:transition-none\">\n                  {item.content}\n                </div>\n              </div>\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\n/** The header a key moves focus to: arrows step and wrap, Home and End jump to the ends. `null` for other keys. */\nfunction headerTarget(key: string, index: number, count: number): number | null {\n  if (count === 0) return null;\n  if (key === \"ArrowDown\") return (index + 1) % count;\n  if (key === \"ArrowUp\") return (index - 1 + count) % count;\n  if (key === \"Home\") return 0;\n  if (key === \"End\") return count - 1;\n  return null;\n}\n\n/** The 14px plus sign whose vertical bar folds away as the item opens, shifting from foreground to accent. */\nfunction PlusMinus({ open }: { open: boolean }) {\n  // an item that starts open shows the minus sign at once; only later changes animate\n  const progress = React.useRef(open ? 1 : 0);\n  const { ref, redraw } = usePixelCanvas(({ context, palette, delta, clear }) => {\n    const target = open ? 1 : 0;\n    progress.current = prefersReducedMotion()\n      ? target\n      : progress.current + clamp(target - progress.current, -delta * 5, delta * 5);\n    const v = progress.current;\n    clear();\n    context.fillStyle = rgba(mixRgb(palette.foreground, palette.accent, v));\n    for (let k = -2; k <= 2; k++) context.fillRect(5.5 + k * 2.6, 5.5, 2.4, 2.4);\n    const s = 1 - v;\n    for (let k = -2; k <= 2; k++) if (k) context.fillRect(5.5, 5.5 + k * 2.6 * s, 2.4, 2.4);\n    return Math.abs(target - progress.current) > 0.001;\n  });\n  React.useEffect(redraw, [open, redraw]);\n  return <canvas ref={ref} aria-hidden=\"true\" className=\"size-3.5 flex-none\" />;\n}\n",
      "type": "registry:component",
      "target": "@components/lumesec/px-accordion.tsx"
    }
  ],
  "meta": {
    "tag": "px-accordion",
    "collection": "pixel-ui",
    "react": "PxAccordion",
    "kind": "react"
  },
  "docs": "Accordion docs: https://elements.lumesec.ai/components/pixel-ui/accordion",
  "categories": [
    "pixel-ui",
    "navigation-data"
  ],
  "type": "registry:component"
}