{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dotted-accordion",
  "type": "registry:ui",
  "title": "Dotted Accordion",
  "description": "Rectangular architectural accordion with contiguous flush items and continuous dotted crosshair guidelines exceeding bounds with smooth gradient masks.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/dotted-accordion.tsx",
      "content": "\"use client\";\n\nimport React, {\n  useState,\n  useRef,\n  useEffect,\n  useMemo,\n  forwardRef,\n  useId,\n  useImperativeHandle,\n} from \"react\";\nimport { motion, AnimatePresence, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface DottedAccordionItem {\n  title: string;\n  description: string;\n}\n\nexport interface DottedAccordionProps extends Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onToggle\"\n> {\n  items: DottedAccordionItem[];\n  defaultIndex?: number | null;\n  collapsible?: boolean;\n  extensionLength?: number;\n  closeOnClickOutside?: boolean;\n  onToggle?: (index: number | null) => void;\n}\n\nexport const DottedAccordion = forwardRef<HTMLDivElement, DottedAccordionProps>(\n  (\n    {\n      items,\n      className,\n      defaultIndex = 0,\n      collapsible = true,\n      extensionLength = 48,\n      closeOnClickOutside = true,\n      onToggle,\n      ...props\n    },\n    ref,\n  ) => {\n    const [activeIndex, setActiveIndex] = useState<number | null>(defaultIndex);\n    const innerRef = useRef<HTMLDivElement>(null);\n    const prefersReducedMotion = useReducedMotion();\n    const baseId = useId();\n\n    useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);\n\n    useEffect(() => {\n      if (!closeOnClickOutside) return;\n\n      const handleClickOutside = (event: MouseEvent) => {\n        if (\n          innerRef.current &&\n          !innerRef.current.contains(event.target as Node)\n        ) {\n          setActiveIndex(null);\n          onToggle?.(null);\n        }\n      };\n\n      document.addEventListener(\"mousedown\", handleClickOutside);\n      return () =>\n        document.removeEventListener(\"mousedown\", handleClickOutside);\n    }, [closeOnClickOutside, onToggle]);\n\n    const toggleAccordion = (index: number) => {\n      const nextIndex =\n        activeIndex === index ? (collapsible ? null : index) : index;\n      setActiveIndex(nextIndex);\n      onToggle?.(nextIndex);\n    };\n\n    const horizontalMaskStyle = useMemo(\n      () => ({\n        WebkitMaskImage: `linear-gradient(to right, transparent, black ${extensionLength}px, black calc(100% - ${extensionLength}px), transparent)`,\n        maskImage: `linear-gradient(to right, transparent, black ${extensionLength}px, black calc(100% - ${extensionLength}px), transparent)`,\n      }),\n      [extensionLength],\n    );\n\n    const verticalMaskStyle = useMemo(\n      () => ({\n        WebkitMaskImage: `linear-gradient(to bottom, transparent, black ${extensionLength}px, black calc(100% - ${extensionLength}px), transparent)`,\n        maskImage: `linear-gradient(to bottom, transparent, black ${extensionLength}px, black calc(100% - ${extensionLength}px), transparent)`,\n      }),\n      [extensionLength],\n    );\n\n    return (\n      <div className=\"@container/accordion relative w-full overflow-clip select-none\">\n        <div\n          ref={innerRef}\n          className={cn(\n            \"relative mx-auto my-6 w-[calc(100%-2rem)] @lg/accordion:my-12 @lg/accordion:w-[calc(100%-7rem)] max-w-xl rounded-none border border-zinc-200 bg-white dark:border-white/10 dark:bg-black\",\n            className,\n          )}\n          {...props}\n        >\n          <div className=\"flex flex-col gap-0 rounded-none divide-y divide-zinc-200 dark:divide-white/10 relative z-10\">\n            {items.map((item, index) => {\n              const isOpen = activeIndex === index;\n              const triggerId = `${baseId}-trigger-${index}`;\n              const panelId = `${baseId}-panel-${index}`;\n\n              return (\n                <div\n                  key={index}\n                  className={cn(\n                    \"relative rounded-none transition-colors duration-200\",\n                    isOpen\n                      ? \"bg-zinc-50 dark:bg-[#111116]\"\n                      : \"bg-white hover:bg-zinc-50/70 dark:bg-[#0a0a0c] dark:hover:bg-[#0f0f13]\",\n                  )}\n                >\n                  <AnimatePresence>\n                    {isOpen && (\n                      <>\n                        <motion.div\n                          initial={{ opacity: 0 }}\n                          animate={{ opacity: 1 }}\n                          exit={{ opacity: 0 }}\n                          transition={{ duration: 0.2 }}\n                          className=\"absolute top-0 -left-12 -right-12 h-0 border-t border-dotted border-zinc-900/70 dark:border-white/70 pointer-events-none z-20\"\n                          style={horizontalMaskStyle}\n                        />\n                        <motion.div\n                          initial={{ opacity: 0 }}\n                          animate={{ opacity: 1 }}\n                          exit={{ opacity: 0 }}\n                          transition={{ duration: 0.2 }}\n                          className=\"absolute bottom-0 -left-12 -right-12 h-0 border-b border-dotted border-zinc-900/70 dark:border-white/70 pointer-events-none z-20\"\n                          style={horizontalMaskStyle}\n                        />\n                        <motion.div\n                          initial={{ opacity: 0 }}\n                          animate={{ opacity: 1 }}\n                          exit={{ opacity: 0 }}\n                          transition={{ duration: 0.2 }}\n                          className=\"absolute left-0 -top-12 -bottom-12 w-0 border-l border-dotted border-zinc-900/70 dark:border-white/70 pointer-events-none z-20\"\n                          style={verticalMaskStyle}\n                        />\n                        <motion.div\n                          initial={{ opacity: 0 }}\n                          animate={{ opacity: 1 }}\n                          exit={{ opacity: 0 }}\n                          transition={{ duration: 0.2 }}\n                          className=\"absolute right-0 -top-12 -bottom-12 w-0 border-r border-dotted border-zinc-900/70 dark:border-white/70 pointer-events-none z-20\"\n                          style={verticalMaskStyle}\n                        />\n                      </>\n                    )}\n                  </AnimatePresence>\n\n                  <button\n                    type=\"button\"\n                    id={triggerId}\n                    aria-expanded={isOpen}\n                    aria-controls={isOpen ? panelId : undefined}\n                    onClick={() => toggleAccordion(index)}\n                    className=\"group flex w-full cursor-pointer items-center justify-between px-3 @lg/accordion:px-5 py-4 text-left rounded-none transition-colors outline-none focus-visible:ring-1 focus-visible:ring-foreground/30 dark:focus-visible:ring-white/30\"\n                  >\n                    <div className=\"flex items-center gap-2 @lg/accordion:gap-3.5 min-w-0 flex-1\">\n                      <span className=\"font-mono text-xs text-zinc-400 group-hover:text-zinc-600 dark:text-zinc-500 dark:group-hover:text-zinc-400 transition-colors shrink-0 w-5\">\n                        {String(index + 1).padStart(2, \"0\")}\n                      </span>\n                      <span\n                        className={cn(\n                          \"text-sm font-medium tracking-tight transition-colors duration-200 whitespace-normal break-words\",\n                          isOpen\n                            ? \"text-zinc-950 dark:text-white\"\n                            : \"text-zinc-700 group-hover:text-zinc-950 dark:text-zinc-300 dark:group-hover:text-white\",\n                        )}\n                      >\n                        {item.title}\n                      </span>\n                    </div>\n\n                    <div className=\"relative flex items-center justify-center w-5 h-5 text-zinc-400 group-hover:text-zinc-700 dark:text-zinc-500 dark:group-hover:text-zinc-200 transition-colors shrink-0 ml-3\">\n                      <motion.span\n                        className=\"absolute h-[1.5px] w-3 bg-current rounded-none\"\n                        animate={{ rotate: isOpen ? 180 : 0 }}\n                        transition={\n                          prefersReducedMotion\n                            ? { duration: 0 }\n                            : { type: \"spring\", stiffness: 320, damping: 24 }\n                        }\n                      />\n                      <motion.span\n                        className=\"absolute w-[1.5px] h-3 bg-current rounded-none\"\n                        animate={{\n                          scaleY: isOpen ? 0 : 1,\n                          opacity: isOpen ? 0 : 1,\n                          rotate: isOpen ? 90 : 0,\n                        }}\n                        transition={\n                          prefersReducedMotion\n                            ? { duration: 0 }\n                            : { type: \"spring\", stiffness: 320, damping: 24 }\n                        }\n                      />\n                    </div>\n                  </button>\n\n                  <AnimatePresence initial={false}>\n                    {isOpen && (\n                      <motion.div\n                        initial={{ height: 0, opacity: 0 }}\n                        animate={{\n                          height: \"auto\",\n                          opacity: 1,\n                          transition: prefersReducedMotion\n                            ? { duration: 0 }\n                            : {\n                                height: {\n                                  duration: 0.32,\n                                  ease: [0.16, 1, 0.3, 1],\n                                },\n                                opacity: {\n                                  duration: 0.22,\n                                  ease: \"easeOut\",\n                                },\n                              },\n                        }}\n                        exit={{\n                          height: 0,\n                          opacity: 0,\n                          transition: prefersReducedMotion\n                            ? { duration: 0 }\n                            : {\n                                height: {\n                                  duration: 0.25,\n                                  ease: [0.16, 1, 0.3, 1],\n                                },\n                                opacity: {\n                                  duration: 0.15,\n                                  ease: \"easeIn\",\n                                },\n                              },\n                        }}\n                        id={panelId}\n                        role=\"region\"\n                        aria-labelledby={triggerId}\n                        className=\"overflow-hidden rounded-none\"\n                      >\n                        <div\n                          className=\"cursor-pointer pl-10 @lg/accordion:pl-13.5 pr-3 @lg/accordion:pr-5 pb-5 pt-0.5 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 font-light\"\n                          onClick={() => toggleAccordion(index)}\n                        >\n                          {item.description}\n                        </div>\n                      </motion.div>\n                    )}\n                  </AnimatePresence>\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      </div>\n    );\n  },\n);\n\nDottedAccordion.displayName = \"DottedAccordion\";\n\nexport default DottedAccordion;\n",
      "type": "registry:ui",
      "target": "@ui/dotted-accordion.tsx"
    }
  ]
}
