{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "recall",
  "type": "registry:component",
  "title": "Recall",
  "description": "Everything the assistant remembers about you, sorted into groups you can filter, with each memory yours to edit, pause or forget, and an undo if you slip.",
  "categories": [
    "component",
    "Trust & Control"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/switch.json"
  ],
  "files": [
    {
      "path": "registry/devl/recall.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { BrainIcon, CheckIcon, PauseIcon, PencilIcon, PlayIcon, Trash2Icon, Undo2Icon, XIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useId, useRef, useState } from \"react\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface Memory {\n  id: string;\n  text: string;\n  category: string;\n  source?: string;\n  paused?: boolean;\n}\n\nexport interface RecallProps {\n  memories: Memory[];\n  assistantName?: string;\n  categories?: string[];\n  enabled?: boolean;\n  onEnabledChange?: (enabled: boolean) => void;\n  onChange?: (memories: Memory[]) => void;\n  undoMs?: number;\n  className?: string;\n}\n\nconst spring = { type: \"spring\", stiffness: 420, damping: 34 } as const;\n\nfunction IconButton({\n  label,\n  onClick,\n  children,\n  danger,\n}: {\n  label: string;\n  onClick: () => void;\n  children: ReactNode;\n  danger?: boolean;\n}) {\n  return (\n    <button\n      aria-label={label}\n      className={cn(\n        \"relative flex size-7 cursor-pointer items-center justify-center rounded-lg text-muted-foreground outline-none transition-[color,background-color,scale] duration-150 ease-out after:absolute after:-inset-0.5 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97] [&_svg]:size-3.5\",\n        danger ? \"hover:bg-destructive/10 hover:text-destructive-foreground\" : \"hover:bg-accent hover:text-foreground\",\n      )}\n      onClick={onClick}\n      title={label}\n      type=\"button\"\n    >\n      {children}\n    </button>\n  );\n}\n\nfunction MemoryRow({\n  memory,\n  onSave,\n  onTogglePause,\n  onForget,\n}: {\n  memory: Memory;\n  onSave: (text: string) => void;\n  onTogglePause: () => void;\n  onForget: () => void;\n}) {\n  const reduce = useReducedMotion();\n  const [editing, setEditing] = useState(false);\n  const [draft, setDraft] = useState(memory.text);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    if (editing) {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    }\n  }, [editing]);\n\n  const save = () => {\n    const t = draft.trim();\n    if (t) onSave(t);\n    else setDraft(memory.text);\n    setEditing(false);\n  };\n\n  return (\n    <div className=\"group/mem relative flex items-start gap-2 rounded-xl px-2.5 py-2 transition-colors hover:bg-accent/50 has-focus-visible:bg-accent/50\">\n      <motion.span\n        animate={{ scale: memory.paused ? 0.7 : 1 }}\n        aria-hidden\n        className={cn(\n          \"mt-[7px] size-1.5 shrink-0 rounded-full transition-colors\",\n          memory.paused ? \"bg-muted-foreground/40\" : \"bg-success\",\n        )}\n        transition={spring}\n      />\n      <div className=\"min-w-0 flex-1\">\n        {editing ? (\n          <form\n            className=\"flex items-center gap-1\"\n            onSubmit={(e) => {\n              e.preventDefault();\n              save();\n            }}\n          >\n            <input\n              aria-label=\"Edit memory\"\n              className=\"h-7 min-w-0 flex-1 rounded-lg border border-ring bg-background px-2 text-sm outline-none ring-[3px] ring-ring/24 dark:bg-input/32\"\n              onChange={(e) => setDraft(e.target.value)}\n              onKeyDown={(e) => {\n                if (e.key === \"Escape\") {\n                  setDraft(memory.text);\n                  setEditing(false);\n                }\n              }}\n              ref={inputRef}\n              value={draft}\n            />\n            <IconButton label=\"Save\" onClick={save}>\n              <CheckIcon />\n            </IconButton>\n            <IconButton\n              label=\"Cancel\"\n              onClick={() => {\n                setDraft(memory.text);\n                setEditing(false);\n              }}\n            >\n              <XIcon />\n            </IconButton>\n          </form>\n        ) : (\n          <>\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.p\n                animate={{ opacity: 1, filter: \"blur(0px)\" }}\n                className={cn(\n                  \"text-pretty text-sm transition-colors\",\n                  memory.paused ? \"text-muted-foreground\" : \"text-foreground\",\n                )}\n                initial={reduce ? false : { opacity: 0, filter: \"blur(3px)\" }}\n                key={memory.text}\n                transition={{ duration: 0.25 }}\n              >\n                {memory.text}\n              </motion.p>\n            </AnimatePresence>\n            <p className=\"mt-0.5 flex items-center gap-1.5 text-muted-foreground text-xs\">\n              <AnimatePresence initial={false}>\n                {memory.paused && (\n                  <motion.span\n                    animate={{ opacity: 1, width: \"auto\" }}\n                    className=\"overflow-hidden whitespace-nowrap rounded-full bg-warning/12 px-1.5 font-medium text-[10px] text-warning-foreground\"\n                    exit={{ opacity: 0, width: 0, paddingLeft: 0, paddingRight: 0 }}\n                    initial={reduce ? false : { opacity: 0, width: 0 }}\n                    transition={spring}\n                  >\n                    Paused\n                  </motion.span>\n                )}\n              </AnimatePresence>\n              {memory.source}\n            </p>\n          </>\n        )}\n      </div>\n      {!editing && (\n        <div className=\"flex shrink-0 items-center transition-opacity duration-150 group-focus-within/mem:opacity-100 group-hover/mem:opacity-100 [@media(hover:hover)]:opacity-0\">\n          <IconButton label=\"Edit memory\" onClick={() => setEditing(true)}>\n            <PencilIcon />\n          </IconButton>\n          <IconButton label={memory.paused ? \"Resume memory\" : \"Pause memory\"} onClick={onTogglePause}>\n            {memory.paused ? <PlayIcon /> : <PauseIcon />}\n          </IconButton>\n          <IconButton danger label=\"Forget memory\" onClick={onForget}>\n            <Trash2Icon />\n          </IconButton>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport function Recall({\n  memories: initial,\n  assistantName = \"your assistant\",\n  categories: categoriesProp,\n  enabled: enabledProp,\n  onEnabledChange,\n  onChange,\n  undoMs = 6000,\n  className,\n}: RecallProps) {\n  const reduce = useReducedMotion();\n  const chipId = useId();\n  const switchId = useId();\n  const [memories, setMemories] = useState(initial);\n  const [enabledState, setEnabledState] = useState(enabledProp ?? true);\n  const enabled = enabledProp ?? enabledState;\n  const [filter, setFilter] = useState<string>(\"All\");\n  const [tray, setTray] = useState<{ memory: Memory; index: number }[]>([]);\n\n  const categories = categoriesProp ?? [...new Set(initial.map((m) => m.category))];\n  const listRef = useRef<HTMLDivElement>(null);\n\n  // Set imperatively: an `inert` prop trips up hydration.\n  useEffect(() => {\n    if (listRef.current) listRef.current.inert = !enabled;\n  }, [enabled]);\n\n  useEffect(() => {\n    if (!tray.length) return;\n    const t = setTimeout(() => setTray([]), undoMs);\n    return () => clearTimeout(t);\n  }, [tray, undoMs]);\n\n  const latestMemories = useRef(memories);\n  latestMemories.current = memories;\n\n  const update = (next: Memory[]) => {\n    latestMemories.current = next;\n    setMemories(next);\n    onChange?.(next);\n  };\n\n  const patch = (id: string, p: Partial<Memory>) =>\n    update(latestMemories.current.map((m) => (m.id === id ? { ...m, ...p } : m)));\n\n  const forget = (id: string) => {\n    const list = latestMemories.current;\n    const index = list.findIndex((m) => m.id === id);\n    if (index < 0) return;\n    setTray((t) => [...t, { memory: list[index], index }]);\n    update(list.filter((m) => m.id !== id));\n  };\n\n  const undo = () => {\n    const next = [...latestMemories.current];\n    for (const { memory, index } of [...tray].reverse()) {\n      next.splice(Math.min(index, next.length), 0, memory);\n    }\n    update(next);\n    setTray([]);\n  };\n\n  const shownCats = filter === \"All\" ? categories : [filter];\n  const counts = Object.fromEntries(categories.map((c) => [c, memories.filter((m) => m.category === c).length]));\n\n  return (\n    <section\n      aria-label=\"Memory\"\n      className={cn(\n        \"relative w-full max-w-[420px] overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-xs/5\",\n        className,\n      )}\n    >\n      <header className=\"flex items-center gap-3 p-4\">\n        <span className=\"flex size-9 shrink-0 items-center justify-center rounded-xl bg-muted\">\n          <BrainIcon className=\"size-4\" />\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          <h2 className=\"font-medium text-sm\">Memory</h2>\n          <p className=\"truncate text-muted-foreground text-xs\">What {assistantName} remembers about you</p>\n        </div>\n        <label className=\"sr-only\" htmlFor={switchId}>\n          Use memory\n        </label>\n        <Switch\n          checked={enabled}\n          id={switchId}\n          onCheckedChange={(v) => {\n            setEnabledState(v);\n            onEnabledChange?.(v);\n          }}\n        />\n      </header>\n\n      <AnimatePresence initial={false}>\n        {!enabled && (\n          <motion.p\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"overflow-hidden px-4 text-muted-foreground text-xs\"\n            exit={{ height: 0, opacity: 0 }}\n            initial={reduce ? false : { height: 0, opacity: 0 }}\n            transition={spring}\n          >\n            <span className=\"mb-3 block rounded-xl bg-warning/10 px-3 py-2 text-warning-foreground\">\n              Memory is off. {assistantName.charAt(0).toUpperCase() + assistantName.slice(1)} won't use or save anything\n              new until you turn it back on.\n            </span>\n          </motion.p>\n        )}\n      </AnimatePresence>\n\n      <div\n        aria-disabled={!enabled}\n        className={cn(\"transition-opacity duration-300\", !enabled && \"pointer-events-none opacity-45\")}\n        ref={listRef}\n      >\n        <div\n          aria-label=\"Filter by category\"\n          className=\"flex gap-1.5 overflow-x-auto px-4 pb-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n          role=\"toolbar\"\n        >\n          {[\"All\", ...categories].map((c) => {\n            const active = filter === c;\n            return (\n              <button\n                aria-pressed={active}\n                className={cn(\n                  \"relative flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border px-2.5 font-medium text-xs outline-none transition-[color,scale] duration-150 ease-out focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]\",\n                  active ? \"border-transparent text-primary-foreground\" : \"text-muted-foreground hover:text-foreground\",\n                )}\n                key={c}\n                onClick={() => setFilter(c)}\n                type=\"button\"\n              >\n                {active && (\n                  <motion.span\n                    className=\"absolute inset-0 rounded-full bg-primary\"\n                    layoutId={`${chipId}-chip`}\n                    transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 480, damping: 30 }}\n                  />\n                )}\n                <span className=\"relative\">{c}</span>\n                <span className=\"relative tabular-nums opacity-60\">\n                  {c === \"All\" ? memories.length : counts[c]}\n                </span>\n              </button>\n            );\n          })}\n        </div>\n\n        <div className=\"h-[300px] overflow-y-auto border-t px-2 pb-16 [mask-image:linear-gradient(to_bottom,black_calc(100%-32px),transparent)]\">\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            {shownCats.map((cat) => {\n              const list = memories.filter((m) => m.category === cat);\n              return (\n                <motion.div\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0, y: -6, transition: { duration: 0.12 } }}\n                  initial={reduce ? false : { opacity: 0, y: 8 }}\n                  key={cat}\n                  layout={!reduce}\n                  transition={spring}\n                >\n                  <h3 className=\"px-2.5 pt-3 pb-1 font-medium text-[11px] text-muted-foreground uppercase tracking-wide\">\n                    {cat}\n                  </h3>\n                  <AnimatePresence initial={false}>\n                    {list.map((m) => (\n                      <motion.div\n                        animate={{ opacity: 1, height: \"auto\" }}\n                        className=\"overflow-hidden\"\n                        exit={reduce ? { opacity: 0 } : { opacity: 0, height: 0, x: -16 }}\n                        initial={reduce ? false : { opacity: 0, height: 0 }}\n                        key={m.id}\n                        transition={spring}\n                      >\n                        <MemoryRow\n                          memory={m}\n                          onForget={() => forget(m.id)}\n                          onSave={(text) => patch(m.id, { text })}\n                          onTogglePause={() => patch(m.id, { paused: !m.paused })}\n                        />\n                      </motion.div>\n                    ))}\n                  </AnimatePresence>\n                  {list.length === 0 && (\n                    <p className=\"px-2.5 py-2 text-muted-foreground text-xs\">Nothing remembered here.</p>\n                  )}\n                </motion.div>\n              );\n            })}\n          </AnimatePresence>\n        </div>\n      </div>\n\n      <AnimatePresence>\n        {tray.length > 0 && (\n          <motion.div\n            animate={{ y: 0, opacity: 1 }}\n            className=\"absolute inset-x-1.5 bottom-1.5 flex items-center gap-2 rounded-lg bg-primary py-1.5 ps-3 pe-1.5 text-primary-foreground text-sm shadow-md/10\"\n            exit={reduce ? { opacity: 0 } : { y: 20, opacity: 0 }}\n            initial={reduce ? false : { y: 20, opacity: 0 }}\n            role=\"status\"\n            transition={{ type: \"spring\", stiffness: 460, damping: 30 }}\n          >\n            <Trash2Icon className=\"size-3.5 shrink-0 opacity-70\" />\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.span\n                animate={{ opacity: 1, y: 0 }}\n                className=\"min-w-0 flex-1 truncate\"\n                exit={{ opacity: 0, y: -8 }}\n                initial={reduce ? false : { opacity: 0, y: 8 }}\n                key={tray.length}\n                transition={spring}\n              >\n                {tray.length === 1 ? `Forgot “${tray[0].memory.text}”` : `Forgot ${tray.length} memories`}\n              </motion.span>\n            </AnimatePresence>\n            <button\n              className=\"relative flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 font-medium text-xs outline-none transition-[background-color,scale] duration-150 ease-out after:absolute after:-inset-y-0.5 after:inset-x-0 hover:bg-primary-foreground/12 focus-visible:ring-2 focus-visible:ring-primary-foreground/50 active:scale-[0.97]\"\n              onClick={undo}\n              type=\"button\"\n            >\n              <Undo2Icon className=\"size-3.5\" />\n              Undo\n            </button>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </section>\n  );\n}\n"
    }
  ]
}