{
  "name": "ai-memory",
  "type": "registry:component",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @kokonutui\n * @description: AI Memory\n * @version: 1.0.0\n * @date: 2026-10-21\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { ArrowUp, Bookmark, Check, Pencil, Trash2 } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  MotionConfig,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MemoryItem {\n  id: string;\n  text: string;\n  group: string;\n  /** When it was saved, in words, e.g. \"Today\" or \"Sep 12\". */\n  saved: string;\n}\n\nexport interface MemoryChange {\n  reply: string;\n  /** A fact to save. With `replaces`, it takes the place of an older one. */\n  memory?: { text: string; group: string; replaces?: string };\n}\n\nexport interface AIMemoryProps {\n  title?: string;\n  initialMemories?: MemoryItem[];\n  /** Messages offered as one-tap replies. */\n  suggestions?: string[];\n  /** Answers a message and decides what to remember. Demo when omitted. */\n  remember?: (text: string, memories: MemoryItem[]) => MemoryChange;\n  className?: string;\n}\n\ntype RowState = \"live\" | \"new\" | \"replaced\";\n\ninterface Row extends MemoryItem {\n  state: RowState;\n}\n\ninterface ChatMessage {\n  id: string;\n  role: \"user\" | \"assistant\";\n  text: string;\n  /** Id of the memory this reply saved. */\n  saved?: string;\n  savedText?: string;\n  replaced?: string;\n}\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst WORDS = /\\S+\\s*/g;\nconst GROUPS = [\"About you\", \"Food\", \"Work\"];\n\nconst DEMO_MEMORIES: MemoryItem[] = [\n  { id: \"name\", text: \"Goes by Samantha\", group: \"About you\", saved: \"Aug 3\" },\n  { id: \"city\", text: \"Lives in Berlin\", group: \"About you\", saved: \"Aug 3\" },\n  { id: \"diet\", text: \"Is pescatarian\", group: \"Food\", saved: \"Sep 12\" },\n  {\n    id: \"spice\",\n    text: \"Likes food very spicy\",\n    group: \"Food\",\n    saved: \"Sep 12\",\n  },\n  {\n    id: \"job\",\n    text: \"Product designer at Fieldwork\",\n    group: \"Work\",\n    saved: \"Jul 28\",\n  },\n  {\n    id: \"tone\",\n    text: \"Wants short, direct answers\",\n    group: \"Work\",\n    saved: \"Jul 28\",\n  },\n];\n\nconst DEMO_REPLIES: Record<string, MemoryChange> = {\n  \"I just moved to Lisbon\": {\n    reply:\n      \"Congrats on the move! I'll plan around Lisbon from now on, so weather, events and dinner ideas will be local.\",\n    memory: {\n      text: \"Lives in Lisbon, moved Oct 2026\",\n      group: \"About you\",\n      replaces: \"city\",\n    },\n  },\n  \"I'm vegetarian now\": {\n    reply:\n      \"Noted. Recipes and restaurant picks will be vegetarian from here on, still spicy.\",\n    memory: { text: \"Is vegetarian\", group: \"Food\", replaces: \"diet\" },\n  },\n  \"Please call me Sam\": {\n    reply: \"Sure thing, Sam.\",\n    memory: { text: \"Goes by Sam\", group: \"About you\", replaces: \"name\" },\n  },\n  \"I started at Linear as a design engineer\": {\n    reply:\n      \"Big news, congrats! I'll keep that in mind when you ask about design systems or code.\",\n    memory: {\n      text: \"Design engineer at Linear\",\n      group: \"Work\",\n      replaces: \"job\",\n    },\n  },\n  \"My daughter Ava starts school next year\": {\n    reply:\n      \"Exciting! I'll remember that when you ask about schools or planning next year.\",\n    memory: {\n      text: \"Has a daughter, Ava, starting school next year\",\n      group: \"About you\",\n    },\n  },\n};\n\nconst demoRemember = (text: string): MemoryChange =>\n  DEMO_REPLIES[text] ?? {\n    reply: \"Got it. Nothing in that I need to remember.\",\n  };\n\nlet counter = 0;\nconst uid = (prefix: string) => {\n  counter += 1;\n  return `${prefix}-${counter}`;\n};\n\nexport default function AIMemory({\n  title = \"Assistant\",\n  initialMemories = DEMO_MEMORIES,\n  suggestions = Object.keys(DEMO_REPLIES),\n  remember = demoRemember,\n  className,\n}: AIMemoryProps) {\n  const reduceMotion = useReducedMotion();\n  const [rows, setRows] = useState<Row[]>(() =>\n    initialMemories.map((m) => ({ ...m, state: \"live\" }))\n  );\n  const [messages, setMessages] = useState<ChatMessage[]>([\n    {\n      id: \"a-0\",\n      role: \"assistant\",\n      text: \"Hi Samantha. What's new? Tell me anything and I'll keep what matters.\",\n    },\n  ]);\n  const [used, setUsed] = useState<string[]>([]);\n  const [draft, setDraft] = useState(\"\");\n  const [shown, setShown] = useState(Number.POSITIVE_INFINITY);\n  const [pending, setPending] = useState<MemoryChange | null>(null);\n  const [enabled, setEnabled] = useState(true);\n  const [flash, setFlash] = useState<string | null>(null);\n  const [undo, setUndo] = useState<{ row: Row; index: number } | null>(null);\n  const [incoming, setIncoming] = useState<{ row: Row; after: string } | null>(\n    null\n  );\n  const threadRef = useRef<HTMLDivElement>(null);\n  const memoryRef = useRef<HTMLElement>(null);\n  const undoRef = useRef<HTMLButtonElement>(null);\n  // Forget and Undo each take away the button that was pressed; focus moves\n  // to the button that replaces it.\n  const focusNext = useRef<\"undo\" | { row: string } | null>(null);\n  useEffect(() => {\n    const target = focusNext.current;\n    if (!target) {\n      return;\n    }\n    focusNext.current = null;\n    if (target === \"undo\") {\n      undoRef.current?.focus();\n    } else {\n      memoryRef.current\n        ?.querySelector<HTMLElement>(\n          `[data-memory=\"${CSS.escape(target.row)}\"] button`\n        )\n        ?.focus();\n    }\n  });\n  const hadFocus = () =>\n    Boolean(memoryRef.current?.contains(document.activeElement));\n\n  const last = messages.at(-1);\n  const words = useMemo(\n    () => (last?.role === \"assistant\" ? (last.text.match(WORDS) ?? []) : []),\n    [last]\n  );\n  const streaming = last?.role === \"assistant\" && shown < words.length;\n\n  // Stream the reply, then save what it learned.\n  useEffect(() => {\n    if (!(last?.role === \"assistant\" && pending)) {\n      return;\n    }\n    if (shown < words.length && !reduceMotion) {\n      const id = setTimeout(() => setShown((n) => n + 1), 45);\n      return () => clearTimeout(id);\n    }\n    const change = pending;\n    setPending(null);\n    setShown(Number.POSITIVE_INFINITY);\n    if (!(change.memory && enabled)) {\n      return;\n    }\n    const { text, group, replaces } = change.memory;\n    const id = uid(\"mem\");\n    const old = rows.find((r) => r.id === replaces && r.state === \"live\");\n    const next: Row = { id, text, group, saved: \"Today\", state: \"new\" };\n    if (old) {\n      // Strike the old fact first; the new one slides in under it.\n      setRows((list) =>\n        list.map((r) => (r.id === old.id ? { ...r, state: \"replaced\" } : r))\n      );\n      setIncoming({ row: next, after: old.id });\n    } else {\n      setRows((list) => [...list, next]);\n    }\n    setMessages((list) =>\n      list.map((m) =>\n        m.id === last.id\n          ? { ...m, saved: id, savedText: text, replaced: old?.text }\n          : m\n      )\n    );\n  }, [last, pending, shown, words, reduceMotion, enabled, rows]);\n\n  // The replacement lands once the strike has had a beat to read.\n  useEffect(() => {\n    if (!incoming) {\n      return;\n    }\n    const id = setTimeout(\n      () => {\n        setRows((list) => {\n          const copy = [...list];\n          const at = copy.findIndex((r) => r.id === incoming.after);\n          copy.splice(at < 0 ? copy.length : at + 1, 0, incoming.row);\n          return copy;\n        });\n        setIncoming(null);\n      },\n      reduceMotion ? 0 : 320\n    );\n    return () => clearTimeout(id);\n  }, [incoming, reduceMotion]);\n\n  const hasReplaced = rows.some((r) => r.state === \"replaced\");\n  const hasNew = rows.some((r) => r.state === \"new\");\n\n  // Struck facts fold away once they have been read.\n  useEffect(() => {\n    if (!hasReplaced) {\n      return;\n    }\n    const id = setTimeout(\n      () => setRows((list) => list.filter((r) => r.state !== \"replaced\")),\n      reduceMotion ? 0 : 1600\n    );\n    return () => clearTimeout(id);\n  }, [hasReplaced, reduceMotion]);\n\n  // New facts lose their tint.\n  useEffect(() => {\n    if (!hasNew) {\n      return;\n    }\n    const id = setTimeout(\n      () =>\n        setRows((list) =>\n          list.map((r) => (r.state === \"new\" ? { ...r, state: \"live\" } : r))\n        ),\n      2400\n    );\n    return () => clearTimeout(id);\n  }, [hasNew]);\n\n  useEffect(() => {\n    if (!flash) {\n      return;\n    }\n    const id = setTimeout(() => setFlash(null), 1200);\n    return () => clearTimeout(id);\n  }, [flash]);\n\n  useEffect(() => {\n    if (!undo) {\n      return;\n    }\n    const id = setTimeout(() => setUndo(null), 5000);\n    return () => clearTimeout(id);\n  }, [undo]);\n\n  const messageCount = messages.length;\n  const lastSaved = last?.saved;\n  useEffect(() => {\n    const el = threadRef.current;\n    if ((messageCount > 1 || lastSaved) && el) {\n      el.scrollTo({ top: el.scrollHeight, behavior: \"smooth\" });\n    }\n  }, [messageCount, lastSaved]);\n\n  const send = (raw: string) => {\n    const text = raw.trim();\n    if (!text || streaming || pending || incoming) {\n      return;\n    }\n    const change = enabled\n      ? remember(\n          text,\n          rows.filter((r) => r.state !== \"replaced\")\n        )\n      : { reply: \"Memory is off, so I won't save anything from this chat.\" };\n    setUsed((u) => [...u, text]);\n    setDraft(\"\");\n    setMessages((list) => [\n      ...list,\n      { id: uid(\"u\"), role: \"user\", text },\n      { id: uid(\"a\"), role: \"assistant\", text: change.reply },\n    ]);\n    setShown(0);\n    setPending(change);\n  };\n\n  const forget = (row: Row) => {\n    if (hadFocus()) {\n      focusNext.current = \"undo\";\n    }\n    const index = rows.findIndex((r) => r.id === row.id);\n    setRows((list) => list.filter((r) => r.id !== row.id));\n    setUndo({ row, index });\n  };\n\n  const restoreRow = () => {\n    if (!undo) {\n      return;\n    }\n    if (hadFocus()) {\n      focusNext.current = { row: undo.row.id };\n    }\n    setRows((list) => {\n      const copy = [...list];\n      copy.splice(undo.index, 0, { ...undo.row, state: \"live\" });\n      return copy;\n    });\n    setUndo(null);\n  };\n\n  const left = suggestions.filter((s) => !used.includes(s));\n  const live = rows.filter((r) => r.state !== \"replaced\");\n  const groups = [\n    ...GROUPS.filter((g) => rows.some((r) => r.group === g)),\n    ...new Set(rows.map((r) => r.group).filter((g) => !GROUPS.includes(g))),\n  ];\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div\n        className={cn(\n          \"@container w-full max-w-3xl overflow-hidden rounded-2xl bg-white ring-1 ring-black/[0.08] dark:bg-zinc-900 dark:ring-white/10\",\n          className\n        )}\n      >\n        <div className=\"grid @xl:grid-cols-[minmax(0,1fr)_17.5rem]\">\n          <section\n            aria-label=\"Chat\"\n            className=\"flex min-w-0 flex-col border-black/[0.06] @xl:border-r border-b @xl:border-b-0 dark:border-white/[0.08]\"\n          >\n            <header className=\"flex h-11 items-center border-black/[0.06] border-b px-4 dark:border-white/[0.08]\">\n              <h3 className=\"font-medium text-[13px] text-zinc-900 dark:text-zinc-100\">\n                {title}\n              </h3>\n            </header>\n            <div\n              className=\"h-80 space-y-4 overflow-y-auto overscroll-contain px-4 py-4 [scrollbar-width:thin]\"\n              ref={threadRef}\n            >\n              {messages.map((message) => {\n                if (message.role === \"user\") {\n                  return (\n                    <motion.div\n                      animate={{ opacity: 1, y: 0 }}\n                      className=\"flex justify-end\"\n                      initial={{ opacity: 0, y: 8 }}\n                      key={message.id}\n                      transition={{ duration: 0.25, ease: EASE_OUT }}\n                    >\n                      <p className=\"max-w-[85%] rounded-2xl rounded-br-md bg-zinc-100 px-3.5 py-2 text-[13px] text-zinc-800 leading-snug dark:bg-zinc-800 dark:text-zinc-100\">\n                        {message.text}\n                      </p>\n                    </motion.div>\n                  );\n                }\n                const isLast = message === last;\n                const text = isLast\n                  ? words.slice(0, shown).join(\"\")\n                  : message.text;\n                return (\n                  <div className=\"space-y-2\" key={message.id}>\n                    <p className=\"text-[13px] text-zinc-800 leading-relaxed dark:text-zinc-200\">\n                      {text}\n                    </p>\n                    <AnimatePresence>\n                      {message.saved && (\n                        <motion.button\n                          animate={{ opacity: 1, y: 0 }}\n                          className=\"flex max-w-full items-center gap-1.5 rounded-md py-0.5 pr-1.5 text-left text-[12px] text-zinc-500 transition-colors duration-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100\"\n                          initial={{ opacity: 0, y: 4 }}\n                          onClick={() => setFlash(message.saved ?? null)}\n                          transition={{ duration: 0.25, ease: EASE_OUT }}\n                          type=\"button\"\n                        >\n                          <Bookmark\n                            aria-hidden=\"true\"\n                            className=\"size-3 shrink-0 fill-current\"\n                          />\n                          <span className=\"truncate\">\n                            {message.replaced\n                              ? \"Memory updated\"\n                              : \"Saved to memory\"}\n                            <span className=\"text-zinc-400 dark:text-zinc-500\">\n                              {\" \"}\n                              · {message.savedText}\n                            </span>\n                          </span>\n                        </motion.button>\n                      )}\n                    </AnimatePresence>\n                  </div>\n                );\n              })}\n            </div>\n            <div className=\"space-y-2 p-2 pt-0\">\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                {left.length > 0 && (\n                  <motion.div\n                    className={cn(\n                      \"flex flex-wrap gap-1.5 px-1 transition-opacity duration-150\",\n                      (streaming || pending || incoming) && \"opacity-40\"\n                    )}\n                    exit={{ opacity: 0 }}\n                    key=\"chips\"\n                  >\n                    <AnimatePresence initial={false} mode=\"popLayout\">\n                      {left.slice(0, 3).map((s) => (\n                        <motion.button\n                          animate={{ opacity: 1 }}\n                          className=\"h-7 max-w-full truncate rounded-full px-3 text-[12px] text-zinc-600 ring-1 ring-black/[0.08] transition-colors duration-100 hover:bg-zinc-100 hover:text-zinc-900 active:bg-zinc-200/80 dark:text-zinc-300 dark:ring-white/10 dark:active:bg-white/[0.12] dark:hover:bg-white/[0.07] dark:hover:text-white\"\n                          disabled={\n                            streaming || Boolean(pending) || Boolean(incoming)\n                          }\n                          exit={{ opacity: 0, transition: { duration: 0.12 } }}\n                          initial={{ opacity: 0 }}\n                          key={s}\n                          layout\n                          onClick={() => send(s)}\n                          transition={{ duration: 0.2, ease: EASE_OUT }}\n                          type=\"button\"\n                        >\n                          {s}\n                        </motion.button>\n                      ))}\n                    </AnimatePresence>\n                  </motion.div>\n                )}\n              </AnimatePresence>\n              <form\n                className=\"flex items-center gap-2 rounded-xl bg-zinc-50 py-1.5 pr-1.5 pl-3 ring-1 ring-black/[0.07] dark:bg-zinc-800/60 dark:ring-white/[0.08]\"\n                onSubmit={(event) => {\n                  event.preventDefault();\n                  send(draft);\n                }}\n              >\n                <input\n                  aria-label=\"Message\"\n                  className=\"h-8 min-w-0 flex-1 bg-transparent text-[13px] text-zinc-900 outline-hidden placeholder:text-zinc-400 dark:text-zinc-100 dark:placeholder:text-zinc-500 [@media(pointer:coarse)]:text-base\"\n                  onChange={(event) => setDraft(event.target.value)}\n                  placeholder=\"Tell it something about you…\"\n                  value={draft}\n                />\n                <button\n                  aria-label=\"Send\"\n                  className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-zinc-900 text-white transition-[background-color,opacity] duration-150 hover:bg-zinc-700 active:bg-zinc-600 disabled:opacity-30 dark:bg-white dark:text-zinc-900 dark:active:bg-zinc-300 dark:hover:bg-zinc-200\"\n                  disabled={\n                    !draft.trim() ||\n                    streaming ||\n                    Boolean(pending) ||\n                    Boolean(incoming)\n                  }\n                  type=\"submit\"\n                >\n                  <ArrowUp className=\"size-3.5\" />\n                </button>\n              </form>\n            </div>\n          </section>\n\n          <section\n            aria-label=\"Memory\"\n            className=\"flex min-w-0 flex-col bg-zinc-50/60 dark:bg-zinc-950/30\"\n            ref={memoryRef}\n          >\n            <header className=\"flex h-11 items-center gap-2 border-black/[0.06] border-b px-4 dark:border-white/[0.08]\">\n              <h3 className=\"font-medium text-[13px] text-zinc-900 dark:text-zinc-100\">\n                Memory\n              </h3>\n              <span className=\"text-[12px] text-zinc-400 tabular-nums dark:text-zinc-500\">\n                {live.length}\n              </span>\n              <Switch on={enabled} onToggle={() => setEnabled((e) => !e)} />\n            </header>\n            <div\n              className={cn(\n                \"flex-1 space-y-4 px-2 py-3 transition-opacity duration-200\",\n                !enabled && \"opacity-50\"\n              )}\n            >\n              {groups.map((group) => (\n                <div key={group}>\n                  <p className=\"px-2 pb-1 text-[11px] text-zinc-400 dark:text-zinc-500\">\n                    {group}\n                  </p>\n                  <ul>\n                    <AnimatePresence initial={false}>\n                      {rows\n                        .filter((r) => r.group === group)\n                        .map((row) => (\n                          <MemoryRow\n                            flash={flash === row.id}\n                            key={row.id}\n                            onEdit={(text) =>\n                              setRows((list) =>\n                                list.map((r) =>\n                                  r.id === row.id\n                                    ? { ...r, text, saved: \"Today\" }\n                                    : r\n                                )\n                              )\n                            }\n                            onForget={() => forget(row)}\n                            row={row}\n                          />\n                        ))}\n                    </AnimatePresence>\n                  </ul>\n                </div>\n              ))}\n            </div>\n            <div aria-live=\"polite\" className=\"h-10 px-2 pb-2\">\n              <AnimatePresence>\n                {undo && (\n                  <motion.div\n                    animate={{ opacity: 1, y: 0 }}\n                    className=\"flex h-8 items-center gap-2 rounded-lg bg-zinc-900 pr-1 pl-3 text-[12px] text-white dark:bg-zinc-100 dark:text-zinc-900\"\n                    exit={{ opacity: 0, y: 4, transition: { duration: 0.15 } }}\n                    initial={{ opacity: 0, y: 4 }}\n                    transition={{ duration: 0.2, ease: EASE_OUT }}\n                  >\n                    <span className=\"min-w-0 flex-1 truncate\">\n                      Forgot “{undo.row.text}”\n                    </span>\n                    <button\n                      className=\"h-6 shrink-0 rounded-md px-2 font-medium transition-colors duration-100 hover:bg-white/15 active:bg-white/25 dark:active:bg-black/15 dark:hover:bg-black/10\"\n                      onClick={restoreRow}\n                      ref={undoRef}\n                      type=\"button\"\n                    >\n                      Undo\n                    </button>\n                  </motion.div>\n                )}\n              </AnimatePresence>\n            </div>\n          </section>\n        </div>\n      </div>\n    </MotionConfig>\n  );\n}\n\nfunction MemoryRow({\n  row,\n  flash,\n  onEdit,\n  onForget,\n}: {\n  row: Row;\n  flash: boolean;\n  onEdit: (text: string) => void;\n  onForget: () => void;\n}) {\n  const [editing, setEditing] = useState(false);\n  const [value, setValue] = useState(row.text);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const rowRef = useRef<HTMLLIElement>(null);\n  const editRef = useRef<HTMLButtonElement>(null);\n  const replaced = row.state === \"replaced\";\n\n  // Tapping \"Saved to memory\" in the chat brings the fact into view.\n  useEffect(() => {\n    if (flash) {\n      rowRef.current?.scrollIntoView({ block: \"nearest\", behavior: \"smooth\" });\n    }\n  }, [flash]);\n\n  // Enter or Escape closes the field; Edit takes focus back once it returns.\n  const refocus = useRef(false);\n  useEffect(() => {\n    if (editing) {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    } else if (refocus.current) {\n      refocus.current = false;\n      editRef.current?.focus();\n    }\n  }, [editing]);\n\n  const save = () => {\n    const text = value.trim();\n    if (text && text !== row.text) {\n      onEdit(text);\n    } else {\n      setValue(row.text);\n    }\n    setEditing(false);\n  };\n\n  const icon =\n    \"flex size-6 shrink-0 items-center justify-center rounded-md text-zinc-400 outline-hidden transition-colors duration-100 hover:bg-zinc-900/[0.06] hover:text-zinc-800 focus-visible:bg-zinc-900/[0.06] focus-visible:text-zinc-800 active:bg-zinc-900/[0.1] dark:text-zinc-500 dark:active:bg-white/15 dark:focus-visible:bg-white/10 dark:focus-visible:text-zinc-100 dark:hover:bg-white/10 dark:hover:text-zinc-100\";\n\n  return (\n    <motion.li\n      animate={{ opacity: 1, height: \"auto\" }}\n      className=\"overflow-hidden\"\n      data-memory={row.id}\n      exit={{ opacity: 0, height: 0, transition: { duration: 0.2 } }}\n      initial={{ opacity: 0, height: 0 }}\n      ref={rowRef}\n      transition={{ duration: 0.28, ease: EASE_OUT }}\n    >\n      <div\n        className={cn(\n          \"group/mem flex min-h-8 items-center gap-1 rounded-lg py-1 pr-1 pl-2 transition-colors duration-700\",\n          row.state === \"new\" &&\n            \"bg-emerald-500/[0.1] duration-200 dark:bg-emerald-400/[0.12]\",\n          flash && \"bg-zinc-900/[0.06] duration-150 dark:bg-white/[0.08]\"\n        )}\n      >\n        {editing ? (\n          <input\n            aria-label=\"Edit memory\"\n            className=\"h-6 min-w-0 flex-1 rounded-md bg-white px-1.5 text-[12.5px] text-zinc-900 outline-hidden ring-1 ring-black/10 dark:bg-zinc-800 dark:text-zinc-100 dark:ring-white/15\"\n            onBlur={save}\n            onChange={(event) => setValue(event.target.value)}\n            onKeyDown={(event) => {\n              if (event.nativeEvent.isComposing) {\n                return;\n              }\n              if (event.key === \"Enter\") {\n                event.preventDefault();\n                refocus.current = true;\n                save();\n              } else if (event.key === \"Escape\") {\n                event.preventDefault();\n                refocus.current = true;\n                setValue(row.text);\n                setEditing(false);\n              }\n            }}\n            ref={inputRef}\n            value={value}\n          />\n        ) : (\n          <p className=\"min-w-0 flex-1 text-[12.5px] leading-snug\">\n            <span\n              className={cn(\n                \"bg-no-repeat transition-[background-size,color] duration-[400ms] ease-[cubic-bezier(0.23,1,0.32,1)]\",\n                \"bg-[length:0%_1px] bg-[linear-gradient(currentColor,currentColor)] bg-[position:0_55%]\",\n                replaced\n                  ? \"bg-[length:100%_1px] text-zinc-400 dark:text-zinc-500\"\n                  : \"text-zinc-700 dark:text-zinc-200\"\n              )}\n            >\n              {row.text}\n            </span>\n            <span className=\"ml-1.5 whitespace-nowrap text-[11px] text-zinc-400 dark:text-zinc-500\">\n              {replaced ? \"Replaced\" : row.saved}\n            </span>\n          </p>\n        )}\n        {row.state === \"new\" && !editing && (\n          <Check\n            aria-label=\"Just saved\"\n            className=\"size-3.5 shrink-0 text-emerald-600 dark:text-emerald-400\"\n          />\n        )}\n        {!(replaced || editing) && (\n          <div className=\"flex shrink-0 opacity-100 transition-opacity duration-150 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:focus-within:opacity-100 [@media(hover:hover)]:group-hover/mem:opacity-100\">\n            <button\n              aria-label={`Edit “${row.text}”`}\n              className={icon}\n              onClick={() => setEditing(true)}\n              ref={editRef}\n              type=\"button\"\n            >\n              <Pencil className=\"size-3\" />\n            </button>\n            <button\n              aria-label={`Forget “${row.text}”`}\n              className={icon}\n              onClick={onForget}\n              type=\"button\"\n            >\n              <Trash2 className=\"size-3\" />\n            </button>\n          </div>\n        )}\n      </div>\n    </motion.li>\n  );\n}\n\nfunction Switch({ on, onToggle }: { on: boolean; onToggle: () => void }) {\n  return (\n    <button\n      aria-checked={on}\n      aria-label=\"Use memory\"\n      className=\"ml-auto flex items-center gap-2 rounded-md py-1 text-[12px] text-zinc-500 outline-hidden focus-visible:ring-2 focus-visible:ring-zinc-900/20 dark:text-zinc-400 dark:focus-visible:ring-white/20\"\n      onClick={onToggle}\n      role=\"switch\"\n      type=\"button\"\n    >\n      {on ? \"On\" : \"Off\"}\n      <span\n        className={cn(\n          \"relative h-4 w-7 rounded-full transition-colors duration-200\",\n          on ? \"bg-zinc-900 dark:bg-zinc-100\" : \"bg-zinc-300 dark:bg-zinc-600\"\n        )}\n      >\n        <span\n          className={cn(\n            \"absolute top-0.5 left-0.5 size-3 rounded-full bg-white shadow-sm transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] dark:bg-zinc-900\",\n            on && \"translate-x-3\"\n          )}\n        />\n      </span>\n    </button>\n  );\n}\n",
      "path": "/components/kokonutui/ai-memory.tsx",
      "target": "components/kokonutui/ai-memory.tsx"
    }
  ]
}