{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pinned-list",
  "title": "Pinned List",
  "description": "A list of items that animate smoothly to a pinned section using Framer Motion layout animations.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/components/unlumen/pinned-list/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  motion,\n  AnimatePresence,\n  LayoutGroup,\n  type Variants,\n} from \"motion/react\";\nimport { Pin } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface PinnedListItem {\n  id: string;\n  name: string;\n  /** e.g. \"Category · Detail\" */\n  subtitle: string;\n  icon: React.ReactNode;\n}\n\nexport interface PinnedListProps {\n  items: PinnedListItem[];\n  className?: string;\n}\n\nconst itemVariants: Variants = {\n  hidden: { opacity: 0, scale: 0.96, y: -6 },\n  visible: {\n    opacity: 1,\n    scale: 1,\n    y: 0,\n    transition: { type: \"spring\", stiffness: 380, damping: 20, mass: 0.8 },\n  },\n  exit: {\n    opacity: 0,\n    scale: 0.96,\n    y: -4,\n    transition: { duration: 0.18, ease: \"easeIn\" },\n  },\n};\n\nfunction ItemCard({\n  item,\n  pinned,\n  onToggle,\n}: {\n  item: PinnedListItem;\n  pinned: boolean;\n  onToggle: () => void;\n}) {\n  return (\n    <motion.div\n      layoutId={item.id}\n      layout\n      variants={itemVariants}\n      initial=\"hidden\"\n      animate=\"visible\"\n      exit=\"exit\"\n      className={cn(\n        \"flex items-center gap-3 rounded-2xl px-3 py-3.5\",\n        \"bg-muted text-muted-foreground/50\",\n        \"border border-transparent\",\n        pinned && \" bg-blue-100 dark:bg-blue-950/30\",\n      )}\n    >\n      <div\n        className={cn(\n          \"flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl\",\n          \"bg-background \",\n          \"text-foreground/70\",\n        )}\n      >\n        {item.icon}\n      </div>\n\n      <div className=\"min-w-0 flex-1\">\n        <p className=\"truncate text-base font-medium leading-tight text-foreground\">\n          {item.name}\n        </p>\n        <p className=\"truncate text-sm text-muted-foreground\">\n          {item.subtitle}\n        </p>\n      </div>\n\n      <button\n        type=\"button\"\n        onClick={onToggle}\n        aria-label={pinned ? `Unpin ${item.name}` : `Pin ${item.name}`}\n        className={cn(\n          \"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full\",\n          \"transition-colors duration-200\",\n          pinned\n            ? \"bg-blue-500 text-white hover:bg-blue-600\"\n            : \"bg-muted-foreground/10 text-muted-foreground hover:bg-muted-foreground/20\",\n        )}\n      >\n        <Pin\n          size={17}\n          className={cn(\n            \"transition-transform duration-200\",\n            pinned && \"-rotate-45\",\n          )}\n        />\n      </button>\n    </motion.div>\n  );\n}\n\nconst headingVariants: Variants = {\n  hidden: { opacity: 0, y: -6 },\n  visible: {\n    opacity: 1,\n    y: 0,\n    transition: { type: \"spring\", stiffness: 400, damping: 22 },\n  },\n  exit: { opacity: 0, y: -4, transition: { duration: 0.15, ease: \"easeIn\" } },\n};\n\nexport function PinnedList({ items, className }: PinnedListProps) {\n  const [pinnedIds, setPinnedIds] = React.useState<Set<string>>(new Set());\n  // keep section visible until item exit animations finish, then unmount to trigger section exit\n  const [showPinnedSection, setShowPinnedSection] = React.useState(false);\n  const pinnedLengthRef = React.useRef(0);\n\n  const togglePin = (id: string) => {\n    setPinnedIds((prev) => {\n      const next = new Set(prev);\n      if (next.has(id)) {\n        next.delete(id);\n      } else {\n        next.add(id);\n      }\n      return next;\n    });\n  };\n\n  const pinned = items.filter((i) => pinnedIds.has(i.id));\n  const unpinned = items.filter((i) => !pinnedIds.has(i.id));\n\n  pinnedLengthRef.current = pinned.length;\n\n  const [showAllSection, setShowAllSection] = React.useState(true);\n  const unpinnedLengthRef = React.useRef(unpinned.length);\n  unpinnedLengthRef.current = unpinned.length;\n\n  // show as soon as something is pinned; hiding is triggered from inner AnimatePresence onExitComplete\n  React.useEffect(() => {\n    if (pinned.length > 0) setShowPinnedSection(true);\n  }, [pinned.length]);\n\n  React.useEffect(() => {\n    if (unpinned.length > 0) setShowAllSection(true);\n  }, [unpinned.length]);\n\n  return (\n    <LayoutGroup>\n      <motion.div\n        layout\n        className={cn(\"flex w-full flex-col gap-1\", className)}\n      >\n        <AnimatePresence onExitComplete={() => setShowPinnedSection(false)}>\n          {showPinnedSection && (\n            <motion.div\n              key=\"pinned-section\"\n              layout\n              variants={headingVariants}\n              initial=\"hidden\"\n              animate=\"visible\"\n              exit=\"exit\"\n              className=\"flex flex-col gap-1\"\n            >\n              <motion.p\n                layout=\"position\"\n                className=\"px-1 pb-0.5 pt-1 text-sm font-medium text-muted-foreground\"\n              >\n                Pinned Items\n              </motion.p>\n              <AnimatePresence\n                mode=\"popLayout\"\n                onExitComplete={() => {\n                  if (pinnedLengthRef.current === 0)\n                    setShowPinnedSection(false);\n                }}\n              >\n                {pinned.map((item) => (\n                  <ItemCard\n                    key={item.id}\n                    item={item}\n                    pinned\n                    onToggle={() => togglePin(item.id)}\n                  />\n                ))}\n              </AnimatePresence>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        <AnimatePresence onExitComplete={() => setShowAllSection(false)}>\n          {showAllSection && (\n            <motion.div\n              key=\"all-section\"\n              layout\n              variants={headingVariants}\n              initial=\"hidden\"\n              animate=\"visible\"\n              exit=\"exit\"\n              className=\"flex flex-col gap-1\"\n            >\n              <motion.p\n                layout=\"position\"\n                className={cn(\n                  \"px-1 pb-0.5 text-sm font-medium text-muted-foreground\",\n                  pinned.length > 0 ? \"pt-3 \" : \"pt-1\",\n                )}\n              >\n                All Items\n              </motion.p>\n              <AnimatePresence\n                mode=\"popLayout\"\n                onExitComplete={() => {\n                  if (unpinnedLengthRef.current === 0) setShowAllSection(false);\n                }}\n              >\n                {unpinned.map((item) => (\n                  <ItemCard\n                    key={item.id}\n                    item={item}\n                    pinned={false}\n                    onToggle={() => togglePin(item.id)}\n                  />\n                ))}\n              </AnimatePresence>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.div>\n    </LayoutGroup>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/pinned-list.tsx"
    }
  ],
  "type": "registry:ui"
}