{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "memory-viewer",
  "title": "Memory viewer",
  "description": "Memory viewer is a production-ready ai component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "lucide-react",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/ai/memory-viewer.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useRef, useState, type KeyboardEvent } from \"react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { Pencil, Search } from \"lucide-react\";\nimport { LiveRegion } from \"@/components/bjork-ui/_core/a11y\";\nimport { ease, easeCss, springs } from \"@/components/bjork-ui/_core/motion\";\nimport {\n  AI_PANEL,\n  FOCUS_RING,\n  PRESS,\n  useAiTone,\n  useControllable,\n  useReduceMotion,\n  type BjorkTone,\n} from \"@/components/bjork-ui/ai/_shared\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MemoryItem {\n  id: string;\n  /** The remembered fact, as one sentence. */\n  text: string;\n  /** Group heading, such as \"Preferences\" or \"Projects\". */\n  category: string;\n  /** \"chat\" was inferred from a conversation; \"user\" was added by hand. */\n  source: \"chat\" | \"user\";\n  /** ISO date the memory was saved. Shown for chat memories. */\n  savedAt?: string;\n  /** Marks a memory saved recently with a soft accent dot. */\n  recent?: boolean;\n}\n\nexport interface MemoryViewerProps {\n  memories: MemoryItem[];\n  /**\n   * Called once a forget is final: 5 seconds after Forget, unless Undo was pressed. The row hides at once and an\n   * Undo receipt takes its place until then.\n   */\n  onForget?: (id: string) => void;\n  onEdit?: (id: string, text: string) => void;\n  /** Controlled on/off state of memory. */\n  enabled?: boolean;\n  defaultEnabled?: boolean;\n  onEnabledChange?: (enabled: boolean) => void;\n  /** Category order. Categories not listed follow in order of appearance. */\n  categories?: string[];\n  /** Demo/preview only: opens this memory in edit mode on mount. */\n  defaultEditingId?: string;\n  tone?: BjorkTone;\n  className?: string;\n}\n\nexport const SAMPLE_MEMORIES: MemoryItem[] = [\n  { id: \"m1\", category: \"Preferences\", text: \"Prefers concise answers with the code first and the explanation after.\", source: \"chat\", savedAt: \"2026-09-14\" },\n  { id: \"m2\", category: \"Preferences\", text: \"Writes in British English: colour, organise, programme.\", source: \"user\" },\n  { id: \"m3\", category: \"Preferences\", text: \"Likes metric units and 24-hour times.\", source: \"chat\", savedAt: \"2026-08-30\" },\n  { id: \"m4\", category: \"Work\", text: \"Staff product designer at Northwind Atlas, a mapping startup.\", source: \"user\" },\n  { id: \"m5\", category: \"Work\", text: \"Runs design reviews on Thursday afternoons.\", source: \"chat\", savedAt: \"2026-09-02\" },\n  {\n    id: \"m6\",\n    category: \"Projects\",\n    text: \"Building Tideline, an offline-first field notes app in React Native.\",\n    source: \"chat\",\n    savedAt: \"2026-10-06\",\n    recent: true,\n  },\n  {\n    id: \"m7\",\n    category: \"Projects\",\n    text: \"Tideline syncs with CRDTs; do not suggest last-write-wins.\",\n    source: \"chat\",\n    savedAt: \"2026-10-07\",\n    recent: true,\n  },\n  { id: \"m8\", category: \"Personal\", text: \"Training for a half marathon in March.\", source: \"chat\", savedAt: \"2026-07-21\" },\n  { id: \"m9\", category: \"Personal\", text: \"Vegetarian, and cooks most weeknights.\", source: \"user\" },\n];\n\nconst DEFAULT_CATEGORIES = [\"Preferences\", \"Work\", \"Projects\", \"Personal\"];\nconst UNDO_MS = 5000;\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n\nfunction provenance(m: MemoryItem): string {\n  if (m.source === \"user\") return \"You added\";\n  const d = m.savedAt ? /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(m.savedAt) : null;\n  return d ? `From chat · ${MONTHS[Number(d[2]) - 1]} ${Number(d[3])}` : \"From chat\";\n}\n\n/**\n * What the assistant remembers about the user, grouped by category. Each memory can be edited in place or\n * forgotten with a five-second undo. Turning memory off dims the list and explains what that means.\n */\nexport function MemoryViewer({\n  memories,\n  onForget,\n  onEdit,\n  enabled,\n  defaultEnabled = true,\n  onEnabledChange,\n  categories = DEFAULT_CATEGORIES,\n  defaultEditingId,\n  tone: toneProp,\n  className,\n}: MemoryViewerProps) {\n  const { style } = useAiTone(toneProp);\n  const reduce = useReduceMotion();\n  const [isOn, setOn] = useControllable(enabled, defaultEnabled, onEnabledChange);\n  const [query, setQuery] = useState(\"\");\n  const [editingId, setEditingId] = useState<string | null>(defaultEditingId ?? null);\n  const [edits, setEdits] = useState<Record<string, string>>({});\n  const [pending, setPending] = useState<string[]>([]);\n  const [gone, setGone] = useState<ReadonlySet<string>>(() => new Set());\n  const [announce, setAnnounce] = useState(\"\");\n  const titleId = useId();\n  const noteId = useId();\n\n  // A forget still waiting on its undo window is committed if the viewer unmounts.\n  const pendingRef = useRef(pending);\n  const onForgetRef = useRef(onForget);\n  useLayoutEffect(() => {\n    pendingRef.current = pending;\n    onForgetRef.current = onForget;\n  });\n  useEffect(() => () => pendingRef.current.forEach((id) => onForgetRef.current?.(id)), []);\n\n  const live = memories.filter((m) => !gone.has(m.id));\n  const remembered = live.filter((m) => !pending.includes(m.id)).length;\n  const q = query.trim().toLowerCase();\n  const textOf = (m: MemoryItem) => edits[m.id] ?? m.text;\n  const visible = q ? live.filter((m) => textOf(m).toLowerCase().includes(q) || m.category.toLowerCase().includes(q)) : live;\n  const order = [...categories, ...visible.map((m) => m.category).filter((c) => !categories.includes(c))];\n  const groups = order\n    .filter((c, i) => order.indexOf(c) === i)\n    .map((category) => ({ category, items: visible.filter((m) => m.category === category) }))\n    .filter((g) => g.items.length > 0);\n\n  const forget = (m: MemoryItem) => {\n    setPending((p) => [...p, m.id]);\n    if (editingId === m.id) setEditingId(null);\n    setAnnounce(\"Memory forgotten. Undo is available for 5 seconds.\");\n  };\n  const undo = (m: MemoryItem) => {\n    setPending((p) => p.filter((id) => id !== m.id));\n    setAnnounce(\"Memory restored\");\n  };\n  const expire = (m: MemoryItem) => {\n    setPending((p) => p.filter((id) => id !== m.id));\n    setGone((g) => new Set(g).add(m.id));\n    onForget?.(m.id);\n  };\n  const save = (m: MemoryItem, text: string) => {\n    const next = text.trim();\n    setEditingId(null);\n    if (!next || next === textOf(m)) return;\n    setEdits((e) => ({ ...e, [m.id]: next }));\n    onEdit?.(m.id, next);\n    setAnnounce(\"Memory updated\");\n  };\n\n  return (\n    <section\n      aria-labelledby={titleId}\n      className={cn(AI_PANEL, \"@container w-full max-w-[560px] font-bjork-alpha text-[color:var(--bjork-text)]\", className)}\n      style={style}\n    >\n      <header className=\"flex items-center justify-between gap-3 pb-3\">\n        <div className=\"flex min-w-0 items-baseline gap-2\">\n          <h2 id={titleId} className=\"text-[14px] font-semibold leading-5\">\n            Memory\n          </h2>\n          <span className=\"font-mono text-[12px] leading-5 tabular-nums text-[color:var(--bjork-text-muted)]\">\n            {remembered} {remembered === 1 ? \"memory\" : \"memories\"}\n          </span>\n        </div>\n        <MemorySwitch\n          on={isOn}\n          onChange={(next) => {\n            setOn(next);\n            setAnnounce(next ? \"Memory turned on\" : \"Memory turned off\");\n          }}\n        />\n      </header>\n\n      <label className=\"relative mb-1 flex h-9 items-center gap-2 rounded-[10px] border border-[color:var(--bjork-border)] bg-[color:var(--bjork-field)] px-3 focus-within:border-[color:var(--bjork-border-strong)]\">\n        <Search aria-hidden=\"true\" size={14} strokeWidth={1.75} className=\"shrink-0 text-[color:var(--bjork-text-faint)]\" />\n        <span className=\"sr-only\">Search memories</span>\n        <input\n          type=\"search\"\n          value={query}\n          onChange={(e) => setQuery(e.target.value)}\n          onKeyDown={(e) => {\n            if (e.key === \"Escape\" && query) {\n              e.preventDefault();\n              setQuery(\"\");\n            }\n          }}\n          placeholder=\"Search memories\"\n          className=\"h-full min-w-0 flex-1 bg-transparent text-[13px] text-[color:var(--bjork-text)] outline-none placeholder:text-[color:var(--bjork-text-soft)] [&::-webkit-search-cancel-button]:hidden\"\n        />\n      </label>\n\n      <div\n        className=\"grid\"\n        style={{\n          gridTemplateRows: isOn ? \"0fr\" : \"1fr\",\n          transition: reduce ? \"none\" : `grid-template-rows 280ms ${easeCss.drawer}`,\n        }}\n      >\n        <div className=\"min-h-0 overflow-hidden\">\n          <p\n            id={noteId}\n            className=\"mt-2 rounded-[10px] border border-dashed border-[color:var(--bjork-border-strong)] px-3 py-2.5 text-[13px] leading-5 text-[color:var(--bjork-text-muted)]\"\n          >\n            <span className=\"font-medium text-[color:var(--bjork-text)]\">Memory is off.</span> New chats won’t save or use\n            these memories. They stay here until you forget them.\n          </p>\n        </div>\n      </div>\n\n      <div\n        aria-describedby={isOn ? undefined : noteId}\n        className={cn(\"transition-opacity duration-200\", !isOn && \"opacity-45\")}\n      >\n        {live.length === 0 ? (\n          <EmptyState title=\"Nothing remembered yet\" body=\"As you chat, useful details about you and your work show up here.\" />\n        ) : groups.length === 0 ? (\n          <EmptyState title={`No memories match “${query.trim()}”`} body=\"Try another word, or clear the search.\" />\n        ) : (\n          groups.map((group) => (\n            <div key={group.category} className=\"pt-4\" role=\"group\" aria-label={group.category}>\n              <h3 className=\"flex items-baseline gap-2 pb-1 font-mono text-[10px] font-normal uppercase tracking-[0.08em] text-[color:var(--bjork-text-faint)]\">\n                {group.category}\n                <span className=\"tabular-nums\">{group.items.filter((m) => !pending.includes(m.id)).length}</span>\n              </h3>\n              <ul className=\"-mx-1 flex flex-col\">\n                <AnimatePresence initial={false}>\n                  {group.items.map((m) => (\n                    <motion.li\n                      key={m.id}\n                      layout={reduce ? false : \"position\"}\n                      initial={reduce ? false : { opacity: 0, height: 0 }}\n                      animate={{ opacity: 1, height: \"auto\" }}\n                      exit={reduce ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, height: 0, filter: \"blur(3px)\" }}\n                      transition={reduce ? { duration: 0 } : { duration: 0.24, ease: ease.out }}\n                      // The 4px side padding keeps focus rings and the edit glow clear of the clip used by the height animation.\n                      className=\"overflow-hidden border-b border-[color:var(--bjork-border)] px-1 last:border-b-0\"\n                    >\n                      {pending.includes(m.id) ? (\n                        <UndoReceipt memory={m} text={textOf(m)} onUndo={() => undo(m)} onExpire={() => expire(m)} />\n                      ) : (\n                        <MemoryRow\n                          memory={m}\n                          text={textOf(m)}\n                          editing={editingId === m.id}\n                          interactive={isOn}\n                          onStartEdit={() => setEditingId(m.id)}\n                          onCancel={() => setEditingId(null)}\n                          onSave={(t) => save(m, t)}\n                          onForget={() => forget(m)}\n                        />\n                      )}\n                    </motion.li>\n                  ))}\n                </AnimatePresence>\n              </ul>\n            </div>\n          ))\n        )}\n      </div>\n      <LiveRegion message={announce} />\n    </section>\n  );\n}\n\nfunction MemorySwitch({ on, onChange }: { on: boolean; onChange: (on: boolean) => void }) {\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={on}\n      onClick={() => onChange(!on)}\n      className={cn(\"group flex h-8 shrink-0 cursor-pointer items-center gap-2 rounded-full pl-2.5 pr-1.5\", FOCUS_RING)}\n    >\n      <span className=\"font-mono text-[11px] text-[color:var(--bjork-text-muted)]\">{on ? \"On\" : \"Off\"}</span>\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"relative h-5 w-9 rounded-full transition-colors duration-200\",\n          on ? \"bg-[color:var(--bjork-accent)]\" : \"bg-[color:var(--bjork-surface-active)] shadow-[inset_0_0_0_1px_var(--bjork-border-strong)]\",\n        )}\n      >\n        <span\n          className={cn(\n            \"absolute left-0.5 top-0.5 size-4 rounded-full shadow-[0_1px_2px_rgba(0,0,0,0.25)] transition-transform duration-200 ease-out motion-reduce:transition-none\",\n            on ? \"translate-x-4 bg-[color:var(--bjork-accent-foreground)]\" : \"translate-x-0 bg-[color:var(--bjork-text-muted)]\",\n          )}\n        />\n      </span>\n    </button>\n  );\n}\n\ninterface RowProps {\n  memory: MemoryItem;\n  text: string;\n  editing: boolean;\n  interactive: boolean;\n  onStartEdit: () => void;\n  onCancel: () => void;\n  onSave: (text: string) => void;\n  onForget: () => void;\n}\n\nfunction MemoryRow({ memory, text, editing, interactive, onStartEdit, onCancel, onSave, onForget }: RowProps) {\n  const editRef = useRef<HTMLButtonElement>(null);\n  const restoreFocus = useRef(false);\n\n  // After Save or Cancel from the keyboard, focus returns to the Edit button.\n  useEffect(() => {\n    if (editing || !restoreFocus.current) return;\n    restoreFocus.current = false;\n    editRef.current?.focus();\n  }, [editing]);\n\n  if (editing) {\n    return (\n      <EditForm\n        initial={text}\n        onCancel={() => {\n          restoreFocus.current = true;\n          onCancel();\n        }}\n        onSave={(t) => {\n          restoreFocus.current = true;\n          onSave(t);\n        }}\n      />\n    );\n  }\n\n  return (\n    <div className=\"group relative flex items-start gap-3 py-2.5\">\n      <span aria-hidden=\"true\" className=\"flex h-5 w-1.5 shrink-0 items-center\">\n        {memory.recent && <span className=\"size-1.5 rounded-full bg-[color:var(--bjork-accent)] shadow-[0_0_0_3px_var(--bjork-accent-soft)]\" />}\n      </span>\n      <div className=\"min-w-0 flex-1\">\n        <p className=\"text-[14px] leading-5 text-[color:var(--bjork-text)]\">\n          {text}\n          {memory.recent && <span className=\"sr-only\"> (recently added)</span>}\n        </p>\n        <p className=\"mt-0.5 font-mono text-[11px] leading-4 text-[color:var(--bjork-text-faint)]\">{provenance(memory)}</p>\n      </div>\n      <div\n        className={cn(\n          \"flex shrink-0 items-center gap-0.5 transition-opacity duration-150\",\n          \"opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 [@media(hover:none)]:opacity-100\",\n          !interactive && \"pointer-events-none\",\n        )}\n      >\n        <button\n          ref={editRef}\n          type=\"button\"\n          disabled={!interactive}\n          onClick={onStartEdit}\n          aria-label={`Edit: ${text}`}\n          className={cn(\n            \"grid size-7 cursor-pointer place-items-center rounded-[7px] text-[color:var(--bjork-text-muted)] transition-colors duration-150 hover:bg-[color:var(--bjork-surface-active)] hover:text-[color:var(--bjork-text)]\",\n            FOCUS_RING,\n            PRESS,\n          )}\n        >\n          <Pencil aria-hidden=\"true\" size={13} strokeWidth={1.75} />\n        </button>\n        <button\n          type=\"button\"\n          disabled={!interactive}\n          onClick={onForget}\n          aria-label={`Forget: ${text}`}\n          className={cn(\n            \"h-7 cursor-pointer rounded-[7px] px-2 text-[12px] font-medium text-[color:var(--bjork-text-muted)] transition-colors duration-150 hover:bg-[color:color-mix(in_srgb,var(--bjork-error)_10%,transparent)] hover:text-[color:var(--bjork-error)]\",\n            FOCUS_RING,\n            PRESS,\n          )}\n        >\n          Forget\n        </button>\n      </div>\n    </div>\n  );\n}\n\nfunction EditForm({ initial, onCancel, onSave }: { initial: string; onCancel: () => void; onSave: (t: string) => void }) {\n  const [draft, setDraft] = useState(initial);\n  const areaRef = useRef<HTMLTextAreaElement>(null);\n\n  useLayoutEffect(() => {\n    const el = areaRef.current;\n    if (!el) return;\n    el.style.height = \"auto\";\n    el.style.height = `${el.scrollHeight}px`;\n  }, [draft]);\n\n  const onKey = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n    if (e.nativeEvent.isComposing) return;\n    if (e.key === \"Escape\") {\n      e.preventDefault();\n      onCancel();\n    } else if (e.key === \"Enter\" && !e.shiftKey) {\n      e.preventDefault();\n      onSave(draft);\n    }\n  };\n\n  return (\n    <form\n      className=\"flex flex-col gap-2 py-2.5 pl-[18px]\"\n      onSubmit={(e) => {\n        e.preventDefault();\n        onSave(draft);\n      }}\n    >\n      <textarea\n        ref={areaRef}\n        rows={1}\n        value={draft}\n        // Focus the field as it opens, with the caret at the end.\n        autoFocus\n        onFocus={(e) => e.currentTarget.setSelectionRange(draft.length, draft.length)}\n        onChange={(e) => setDraft(e.target.value)}\n        onKeyDown={onKey}\n        aria-label=\"Edit memory\"\n        className=\"block w-full resize-none overflow-hidden rounded-[9px] border border-[color:var(--bjork-accent-muted)] bg-[color:var(--bjork-field)] px-3 py-2 text-[14px] leading-5 text-[color:var(--bjork-text)] shadow-[0_0_0_3px_var(--bjork-accent-soft)] outline-none\"\n      />\n      <div className=\"flex items-center justify-end gap-1.5\">\n        <span className=\"mr-auto hidden font-mono text-[11px] text-[color:var(--bjork-text-faint)] @[400px]:inline\">\n          ↵ save · esc cancel\n        </span>\n        <button\n          type=\"button\"\n          onClick={onCancel}\n          className={cn(\n            \"h-7 cursor-pointer rounded-[7px] px-2.5 text-[12px] font-medium text-[color:var(--bjork-text-muted)] hover:bg-[color:var(--bjork-surface-active)] hover:text-[color:var(--bjork-text)]\",\n            FOCUS_RING,\n            PRESS,\n          )}\n        >\n          Cancel\n        </button>\n        <button\n          type=\"submit\"\n          disabled={!draft.trim()}\n          className={cn(\n            \"h-7 cursor-pointer rounded-[7px] bg-[color:var(--bjork-accent)] px-3 text-[12px] font-medium text-[color:var(--bjork-accent-foreground)] disabled:cursor-not-allowed disabled:opacity-45\",\n            FOCUS_RING,\n            PRESS,\n          )}\n        >\n          Save\n        </button>\n      </div>\n    </form>\n  );\n}\n\nfunction UndoReceipt({\n  memory,\n  text,\n  onUndo,\n  onExpire,\n}: {\n  memory: MemoryItem;\n  text: string;\n  onUndo: () => void;\n  onExpire: () => void;\n}) {\n  const reduce = useReduceMotion();\n  const undoRef = useRef<HTMLButtonElement>(null);\n  const expireRef = useRef(onExpire);\n  useLayoutEffect(() => {\n    expireRef.current = onExpire;\n  });\n\n  useEffect(() => {\n    // Keyboard users who pressed Forget land on Undo.\n    if (document.activeElement === document.body || document.activeElement === null) undoRef.current?.focus();\n    const id = window.setTimeout(() => expireRef.current(), UNDO_MS);\n    return () => window.clearTimeout(id);\n  }, []);\n\n  return (\n    <motion.div\n      initial={reduce ? false : { opacity: 0, filter: \"blur(4px)\" }}\n      animate={{ opacity: 1, filter: \"blur(0px)\" }}\n      transition={reduce ? { duration: 0 } : springs.blurIn}\n      className=\"relative flex items-center gap-3 py-2.5 pl-[18px]\"\n      data-memory={memory.id}\n    >\n      <p className=\"min-w-0 flex-1 truncate text-[13px] leading-5 text-[color:var(--bjork-text-muted)]\">\n        Forgot <span className=\"text-[color:var(--bjork-text-soft)] line-through decoration-[color:var(--bjork-text-faint)]\">{text}</span>\n      </p>\n      <button\n        ref={undoRef}\n        type=\"button\"\n        onClick={onUndo}\n        className={cn(\n          \"h-7 shrink-0 cursor-pointer rounded-[7px] px-2.5 text-[12px] font-medium text-[color:var(--bjork-accent-ink)] hover:bg-[color:var(--bjork-accent-soft)]\",\n          FOCUS_RING,\n          PRESS,\n        )}\n      >\n        Undo\n      </button>\n      <span aria-hidden=\"true\" className=\"absolute inset-x-0 bottom-0 h-px overflow-hidden\">\n        <span\n          className=\"absolute inset-0 origin-left bg-[color:var(--bjork-text-faint)]\"\n          style={{ animation: reduce ? \"none\" : `bjork-memory-undo ${UNDO_MS}ms linear forwards` }}\n        />\n      </span>\n      <style href=\"bjork-memory-undo\" precedence=\"default\">\n        {\"@keyframes bjork-memory-undo{from{transform:scaleX(1)}to{transform:scaleX(0)}}\"}\n      </style>\n    </motion.div>\n  );\n}\n\nfunction EmptyState({ title, body }: { title: string; body: string }) {\n  return (\n    <div className=\"flex flex-col items-center gap-1 px-4 py-10 text-center\">\n      <p className=\"text-[14px] font-medium leading-5 text-[color:var(--bjork-text-medium)]\">{title}</p>\n      <p className=\"max-w-[300px] text-[13px] leading-5 text-[color:var(--bjork-text-muted)]\">{body}</p>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/ai/memory-viewer.tsx"
    },
    {
      "path": "components/bjork-ui/_core/a11y.tsx",
      "content": "import type { ReactNode } from \"react\";\n\n// Text for assistive tech only.\nexport function VisuallyHidden({ children }: { children: ReactNode }) {\n  return <span className=\"sr-only\">{children}</span>;\n}\n\n// Announces `message` when it changes. Same text re-rendered does not re-announce.\nexport function LiveRegion({\n  message,\n  politeness = \"polite\",\n}: {\n  message: string;\n  politeness?: \"polite\" | \"assertive\";\n}) {\n  return (\n    <span role=\"status\" aria-live={politeness} aria-atomic=\"true\" className=\"sr-only\">\n      {message}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/a11y.tsx"
    },
    {
      "path": "components/bjork-ui/_core/motion.ts",
      "content": "// Shared springs, easings and durations. Components import these instead of inventing values.\n\nexport const springs = {\n  press: { type: \"spring\", stiffness: 500, damping: 30, mass: 0.6 },\n  snappy: { type: \"spring\", stiffness: 400, damping: 25, mass: 1 },\n  standard: { type: \"spring\", stiffness: 300, damping: 30, mass: 0.8 },\n  soft: { type: \"spring\", stiffness: 200, damping: 25, mass: 1.2 },\n  settle: { type: \"spring\", stiffness: 380, damping: 30, mass: 0.8 },\n  blurIn: { type: \"spring\", stiffness: 400, damping: 28, mass: 0.6 },\n} as const;\n\nexport const ease = {\n  out: [0.23, 1, 0.32, 1],\n  inOut: [0.77, 0, 0.175, 1],\n  drawer: [0.32, 0.72, 0, 1],\n} as const;\n\nexport const easeCss = {\n  out: \"cubic-bezier(0.23,1,0.32,1)\",\n  inOut: \"cubic-bezier(0.77,0,0.175,1)\",\n  drawer: \"cubic-bezier(0.32,0.72,0,1)\",\n} as const;\n\nexport const durations = { press: 140, tooltip: 160, menu: 200, panel: 280 } as const; // ms\n\n// Cubic bezier easing for canvas and rAF code. Returns progress y for input x in [0, 1].\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number): (t: number) => number {\n  const cx = 3 * x1;\n  const bx = 3 * (x2 - x1) - cx;\n  const ax = 1 - cx - bx;\n  const cy = 3 * y1;\n  const by = 3 * (y2 - y1) - cy;\n  const ay = 1 - cy - by;\n  const sampleX = (s: number) => ((ax * s + bx) * s + cx) * s;\n  const sampleY = (s: number) => ((ay * s + by) * s + cy) * s;\n  const sampleDX = (s: number) => (3 * ax * s + 2 * bx) * s + cx;\n\n  const solveX = (x: number) => {\n    let s = x;\n    for (let i = 0; i < 8; i++) {\n      const err = sampleX(s) - x;\n      if (Math.abs(err) < 1e-6) return s;\n      const d = sampleDX(s);\n      if (Math.abs(d) < 1e-6) break;\n      s -= err / d;\n    }\n    let lo = 0;\n    let hi = 1;\n    s = x;\n    while (lo < hi) {\n      const v = sampleX(s);\n      if (Math.abs(v - x) < 1e-6) return s;\n      if (x > v) lo = s;\n      else hi = s;\n      s = (lo + hi) / 2;\n      if (hi - lo < 1e-7) break;\n    }\n    return s;\n  };\n\n  return (t: number) => {\n    if (t <= 0) return 0;\n    if (t >= 1) return 1;\n    return sampleY(solveX(t));\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/motion.ts"
    },
    {
      "path": "components/bjork-ui/ai/_shared.ts",
      "content": "\"use client\";\n\n// Shared plumbing for the bjork-ui AI components: tone tokens, reduced motion, controllable state and\n// number formatting. Every AI component imports from here so they read as one family.\n\nimport {\n  useCallback,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type CSSProperties,\n} from \"react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport { BJORK_PALETTE, type BjorkTone } from \"@/components/bjork-ui/_core/palette\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport { surfaceVars } from \"@/components/bjork-ui/_core/surface\";\n\nexport type { BjorkTone };\nexport type BjorkPalette = (typeof BJORK_PALETTE)[BjorkTone];\n\nconst noopSubscribe = () => () => {};\n\n/** False on the server and the first client render, true after hydration. */\nexport function useHydrated(): boolean {\n  return useSyncExternalStore(\n    noopSubscribe,\n    () => true,\n    () => false,\n  );\n}\n\n/** Hydration-safe reduced motion: both sides report \"not reduced\" until mount, then the real value applies. */\nexport function useReduceMotion(): boolean {\n  const hydrated = useHydrated();\n  const reduced = useReducedMotion();\n  return hydrated && reduced === true;\n}\n\n/**\n * Resolves the tone and returns the CSS custom properties for it. Spread `style` on the component root so an\n * explicit `tone` prop wins over the page theme, and status colours (success, warning, error) are always defined.\n */\nexport function useAiTone(toneProp?: BjorkTone) {\n  const tone = useBjorkTone(toneProp);\n  const pal = BJORK_PALETTE[tone];\n  const style = {\n    ...surfaceVars(tone),\n    \"--bjork-text\": pal.text,\n    \"--bjork-text-medium\": pal.textMedium,\n    \"--bjork-text-muted\": pal.textMuted,\n    \"--bjork-text-soft\": pal.textSoft,\n    \"--bjork-text-faint\": pal.textFaint,\n    \"--bjork-hair\": pal.hair,\n    \"--bjork-border\": pal.border,\n    \"--bjork-border-strong\": pal.borderStrong,\n    \"--bjork-surface\": pal.surface,\n    \"--bjork-raised\": pal.raised,\n    \"--bjork-surface-active\": pal.active,\n    \"--bjork-accent\": pal.accent,\n    \"--bjork-accent-ink\": pal.accentInk,\n    \"--bjork-accent-fill\": pal.accentFill,\n    \"--bjork-accent-soft\": pal.accentSoft,\n    \"--bjork-accent-muted\": pal.accentMuted,\n    \"--bjork-accent-foreground\": pal.accentFg,\n    \"--bjork-success\": pal.success,\n    \"--bjork-warning\": pal.warning,\n    \"--bjork-error\": pal.error,\n    \"--bjork-ring-offset\": pal.bg,\n    colorScheme: tone,\n  } as CSSProperties;\n  return { tone, pal, style };\n}\n\n/** Classes for the shared keyboard focus ring. */\nexport const FOCUS_RING =\n  \"outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bjork-ring-offset)]\";\n\n/** Classes for the shared press feedback. */\nexport const PRESS = \"transition-transform duration-150 ease-out active:scale-[0.97] motion-reduce:transition-none\";\n\n/**\n * Controlled-or-uncontrolled state. Pass `value` to control it; `onChange` fires for every change either way.\n */\nexport function useControllable<T>(\n  value: T | undefined,\n  defaultValue: T,\n  onChange?: (next: T) => void,\n): [T, (next: T) => void] {\n  const controlled = value !== undefined;\n  const [inner, setInner] = useState(defaultValue);\n  const current = controlled ? value : inner;\n  const onChangeRef = useRef(onChange);\n  useLayoutEffect(() => {\n    onChangeRef.current = onChange;\n  });\n  const set = useCallback(\n    (next: T) => {\n      if (!controlled) setInner(next);\n      onChangeRef.current?.(next);\n    },\n    [controlled],\n  );\n  return [current, set];\n}\n\n/** 1234 -> \"1,234\", 18400 -> \"18.4k\", 1_200_000 -> \"1.2M\". */\nexport function formatTokens(n: number): string {\n  if (n < 10_000) return Math.round(n).toLocaleString(\"en-US\");\n  if (n < 1_000_000) return `${(n / 1000).toFixed(n < 100_000 ? 1 : 0).replace(/\\.0$/, \"\")}k`;\n  return `${(n / 1_000_000).toFixed(n < 10_000_000 ? 2 : 1).replace(/\\.?0+$/, \"\")}M`;\n}\n\n/** US dollars with sub-cent precision when it matters: 0.0042 -> \"$0.0042\", 12.5 -> \"$12.50\". */\nexport function formatCost(usd: number): string {\n  if (usd === 0) return \"$0.00\";\n  if (Math.abs(usd) < 0.01) return `$${usd.toFixed(4)}`;\n  if (Math.abs(usd) < 1) return `$${usd.toFixed(3)}`;\n  return `$${usd.toLocaleString(\"en-US\", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;\n}\n\n/** 640 -> \"640ms\", 4200 -> \"4.2s\", 95000 -> \"1m 35s\". */\nexport function formatDuration(ms: number): string {\n  if (ms < 1000) return `${Math.round(ms)}ms`;\n  if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;\n  const m = Math.floor(ms / 60_000);\n  const s = Math.round((ms % 60_000) / 1000);\n  return `${m}m ${s}s`;\n}\n\n/** 2_400_000 bytes -> \"2.4 MB\". */\nexport function formatBytes(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`;\n  const units = [\"KB\", \"MB\", \"GB\"];\n  let v = bytes / 1024;\n  let i = 0;\n  while (v >= 1024 && i < units.length - 1) {\n    v /= 1024;\n    i++;\n  }\n  return `${v.toFixed(v < 10 ? 1 : 0)} ${units[i]}`;\n}\n\n/** Shimmer keyframes for \"working\" text. Mount once with <style href=... precedence=\"default\">. */\nexport const SHIMMER_TEXT_CSS =\n  \"@keyframes bjork-ai-shimmer{from{background-position:100% 0}to{background-position:0% 0}}\" +\n  \".bjork-ai-shimmer{background-image:linear-gradient(90deg,var(--bjork-text-muted) 0%,var(--bjork-text-muted) 40%,var(--bjork-text) 50%,var(--bjork-text-muted) 60%,var(--bjork-text-muted) 100%);background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:bjork-ai-shimmer 1.8s linear infinite}\" +\n  \"@media (prefers-reduced-motion: reduce){.bjork-ai-shimmer{animation:none;background:none;color:var(--bjork-text-muted)}}\";\n\n/** CANON §2. Panel = a component's own root surface; Card = a standalone card; Tile = a card inside a panel; Well = recessed code/field. */\nexport const AI_PANEL = \"rounded-[20px] border border-[color:var(--bjork-border-muted)] bg-[color:var(--bjork-surface)] p-4 shadow-[var(--bjork-shadow-surface)]\";\nexport const AI_CARD = \"rounded-[18px] border border-[color:var(--bjork-border-muted)] bg-[color:var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)]\";\nexport const AI_TILE = \"rounded-[12px] border border-[color:var(--bjork-border-muted)] bg-[color:var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)]\";\nexport const AI_WELL = \"rounded-[8px] border border-[color:var(--bjork-border)] bg-[color:var(--bjork-field-inset)] shadow-[var(--bjork-shadow-inset)]\";\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/ai/_shared.ts"
    },
    {
      "path": "components/bjork-ui/_core/palette.ts",
      "content": "export type BjorkTone = \"dark\" | \"light\";\n\n// Canvas, WebGL and SVG attribute colours. DOM code uses the CSS tokens with these as fallbacks.\nexport const BJORK_PALETTE = {\n  dark: {\n    bg: \"#050505\", stage: \"#111111\", surface: \"#121212\", raised: \"#161616\", active: \"#202020\",\n    border: \"#232323\", borderStrong: \"#343434\",\n    text: \"#ededed\", textMedium: \"rgba(237,237,237,0.72)\", textMuted: \"rgba(237,237,237,0.6)\",\n    textSoft: \"rgba(237,237,237,0.5)\", textFaint: \"rgba(237,237,237,0.22)\", hair: \"rgba(237,237,237,0.12)\",\n    accent: \"#ec5c13\", accentInk: \"#ec5c13\", accentFill: \"#b84a12\", accentSoft: \"rgba(236,92,19,0.12)\", accentMuted: \"rgba(236,92,19,0.24)\",\n    accentFg: \"#fff2ea\", success: \"#4cc38a\", warning: \"#f2b544\", error: \"#ff5c4d\",\n  },\n  light: {\n    bg: \"#f7f5ef\", stage: \"#f7f5ef\", surface: \"#fffcf6\", raised: \"#f5efe3\", active: \"#efe7d8\",\n    border: \"#eee6db\", borderStrong: \"#e1d7c8\",\n    text: \"#171717\", textMedium: \"rgba(23,23,23,0.78)\", textMuted: \"rgba(23,23,23,0.68)\",\n    textSoft: \"rgba(23,23,23,0.62)\", textFaint: \"rgba(23,23,23,0.22)\", hair: \"rgba(23,23,23,0.10)\",\n    accent: \"#ec7d43\", accentInk: \"#b4531f\", accentFill: \"#ec7d43\", accentSoft: \"rgba(236,125,67,0.10)\", accentMuted: \"rgba(236,125,67,0.20)\",\n    accentFg: \"#3f2112\", success: \"#1f8a55\", warning: \"#a86b00\", error: \"#c8361f\",\n  },\n} as const;\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/palette.ts"
    },
    {
      "path": "components/bjork-ui/_core/tone.ts",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport type { BjorkTone } from \"./palette\";\n\nconst subscribeNoop = () => () => {};\nconst getMounted = () => true;\nconst getServerMounted = () => false;\n\n/**\n * Resolves the tone for a component. An explicit `tone` always wins.\n * Otherwise it follows next-themes' resolved theme, but only after mount,\n * so the server render and first client render agree on dark.\n */\nexport function useBjorkTone(tone?: BjorkTone): BjorkTone {\n  const { resolvedTheme } = useTheme();\n  const mounted = useSyncExternalStore(subscribeNoop, getMounted, getServerMounted);\n  if (tone) return tone;\n  if (!mounted) return \"dark\";\n  return resolvedTheme === \"light\" ? \"light\" : \"dark\";\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/tone.ts"
    },
    {
      "path": "components/bjork-ui/_core/surface.ts",
      "content": "import type { CSSProperties } from \"react\";\nimport type { BjorkTone } from \"./palette\";\n\n// The library surface (CANON.md §1). Values mirror app/globals.css :root and .dark. Change both together.\nexport const BJORK_SURFACE = {\n  light: {\n    surface: \"#fffcf6\", surfaceMuted: \"rgba(250, 246, 237, 0.94)\", surfaceHover: \"#f5efe3\", surfaceActive: \"#efe7d8\",\n    panel: \"#f8f2e7\", menu: \"rgba(255, 252, 246, 0.98)\", field: \"rgba(255, 252, 246, 0.96)\",\n    fieldMuted: \"rgba(248, 242, 231, 0.82)\", fieldInset: \"rgba(239, 231, 216, 0.72)\",\n    border: \"#eee6db\", borderMuted: \"#f5ede2\", borderStrong: \"#e1d7c8\",\n    shadowSurface: \"inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)\",\n    shadowSoft: \"inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)\",\n    shadowPanel: \"inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)\",\n    shadowMenu: \"inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)\",\n    shadowInset: \"inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)\",\n  },\n  dark: {\n    surface: \"#121212\", surfaceMuted: \"rgba(22, 22, 22, 0.92)\", surfaceHover: \"#161616\", surfaceActive: \"#202020\",\n    panel: \"#0d0d0d\", menu: \"rgba(18, 18, 18, 0.98)\", field: \"rgba(18, 18, 18, 0.95)\",\n    fieldMuted: \"rgba(18, 18, 18, 0.55)\", fieldInset: \"rgba(9, 9, 9, 0.75)\",\n    border: \"#232323\", borderMuted: \"#1c1c1c\", borderStrong: \"#343434\",\n    shadowSurface: \"inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)\",\n    shadowSoft: \"inset 0 1px 0 rgba(255, 255, 255, 0.045)\",\n    shadowPanel: \"inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)\",\n    shadowMenu: \"inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)\",\n    shadowInset: \"inset 0 1px 10px rgba(0, 0, 0, 0.45)\",\n  },\n} as const;\n\n/** Writes the surface tokens for a pinned tone as --bjork-* custom properties. Spread first; kit-specific keys may override. */\nexport function surfaceVars(tone: BjorkTone): CSSProperties {\n  const s = BJORK_SURFACE[tone];\n  return {\n    \"--bjork-surface\": s.surface, \"--bjork-surface-muted\": s.surfaceMuted, \"--bjork-surface-hover\": s.surfaceHover,\n    \"--bjork-surface-active\": s.surfaceActive, \"--bjork-panel\": s.panel, \"--bjork-menu\": s.menu, \"--bjork-field\": s.field,\n    \"--bjork-field-muted\": s.fieldMuted, \"--bjork-field-inset\": s.fieldInset, \"--bjork-border\": s.border,\n    \"--bjork-border-muted\": s.borderMuted, \"--bjork-border-strong\": s.borderStrong,\n    \"--bjork-shadow-surface\": s.shadowSurface, \"--bjork-shadow-soft\": s.shadowSoft, \"--bjork-shadow-panel\": s.shadowPanel,\n    \"--bjork-shadow-menu\": s.shadowMenu, \"--bjork-shadow-inset\": s.shadowInset,\n  } as CSSProperties;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/surface.ts"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    }
  ],
  "cssVars": {
    "light": {
      "bjork-bg": "#f7f3ea",
      "bjork-surface": "#fffcf6",
      "bjork-surface-muted": "rgba(250, 246, 237, 0.94)",
      "bjork-surface-hover": "#f5efe3",
      "bjork-surface-active": "#efe7d8",
      "bjork-panel": "#f8f2e7",
      "bjork-menu": "rgba(255, 252, 246, 0.98)",
      "bjork-field": "rgba(255, 252, 246, 0.96)",
      "bjork-field-muted": "rgba(248, 242, 231, 0.82)",
      "bjork-field-inset": "rgba(239, 231, 216, 0.72)",
      "bjork-error-bg": "#fff0e8",
      "bjork-text": "#171717",
      "bjork-text-strong": "rgba(23, 23, 23, 0.9)",
      "bjork-text-medium": "rgba(23, 23, 23, 0.78)",
      "bjork-text-muted": "rgba(23, 23, 23, 0.68)",
      "bjork-text-soft": "rgba(23, 23, 23, 0.62)",
      "bjork-text-faint": "rgba(23, 23, 23, 0.22)",
      "bjork-inverted-text": "#fff8f0",
      "bjork-border": "#eee6db",
      "bjork-border-muted": "#f5ede2",
      "bjork-border-strong": "#e1d7c8",
      "bjork-thumb": "#a49b8e",
      "bjork-track": "#ded6ca",
      "bjork-accent": "#ec7d43",
      "bjork-accent-hover": "#f0935f",
      "bjork-accent-soft": "rgba(236, 125, 67, 0.1)",
      "bjork-accent-muted": "rgba(236, 125, 67, 0.2)",
      "bjork-accent-badge": "rgba(236, 125, 67, 0.16)",
      "bjork-accent-badge-foreground": "#8d421d",
      "bjork-accent-foreground": "#3f2112",
      "bjork-accent-fill": "#ec7d43",
      "bjork-accent-fill-hover": "#f0935f",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)",
      "bjork-ring-offset": "#f7f3ea"
    },
    "dark": {
      "bjork-bg": "#111111",
      "bjork-surface": "#121212",
      "bjork-surface-muted": "rgba(22, 22, 22, 0.92)",
      "bjork-surface-hover": "#161616",
      "bjork-surface-active": "#202020",
      "bjork-panel": "#0d0d0d",
      "bjork-menu": "rgba(18, 18, 18, 0.98)",
      "bjork-field": "rgba(18, 18, 18, 0.95)",
      "bjork-field-muted": "rgba(18, 18, 18, 0.55)",
      "bjork-field-inset": "rgba(9, 9, 9, 0.75)",
      "bjork-error-bg": "#130d0a",
      "bjork-text": "#ededed",
      "bjork-text-strong": "rgba(237, 237, 237, 0.9)",
      "bjork-text-medium": "rgba(237, 237, 237, 0.72)",
      "bjork-text-muted": "rgba(237, 237, 237, 0.6)",
      "bjork-text-soft": "rgba(237, 237, 237, 0.5)",
      "bjork-text-faint": "rgba(237, 237, 237, 0.22)",
      "bjork-inverted-text": "#080808",
      "bjork-border": "#232323",
      "bjork-border-muted": "#1c1c1c",
      "bjork-border-strong": "#343434",
      "bjork-thumb": "#626262",
      "bjork-track": "#090909",
      "bjork-accent": "#ec5c13",
      "bjork-accent-hover": "#f07832",
      "bjork-accent-soft": "rgba(236, 92, 19, 0.12)",
      "bjork-accent-muted": "rgba(236, 92, 19, 0.24)",
      "bjork-accent-badge": "#b84a12",
      "bjork-accent-badge-foreground": "#fff2ea",
      "bjork-accent-foreground": "#fff2ea",
      "bjork-accent-fill": "#b84a12",
      "bjork-accent-fill-hover": "#a84a14",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(255, 255, 255, 0.045)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(0, 0, 0, 0.45)",
      "bjork-ring-offset": "#080808"
    }
  },
  "css": {
    ".bjork-range": {
      "box-shadow": "inset 0 1px 1px rgba(255, 255, 255, 0.04), 0 0 14px rgba(189, 69, 20, 0.06)"
    },
    ".bjork-range::-webkit-slider-thumb": {
      "appearance": "none",
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-range::-moz-range-thumb": {
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-layered-button-accent, .bjork-layered-button-raised": {
      "position": "relative",
      "isolation": "isolate",
      "appearance": "none",
      "overflow": "hidden",
      "border": "0",
      "border-radius": "13px",
      "box-shadow": "inset 0 1px 0 #ffffff38, 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a",
      "transition": "filter 150ms ease-out, box-shadow 150ms ease-out"
    },
    ".bjork-layered-button-accent::after, .bjork-layered-button-raised::after": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none",
      "content": "\"\"",
      "border": "1.5px solid transparent",
      "border-radius": "inherit",
      "background": "linear-gradient(#ffffffb8, #0000003d 41% 75%, #ffffff47) border-box",
      "mix-blend-mode": "overlay",
      "-webkit-mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "-webkit-mask-clip": "padding-box, border-box",
      "mask-clip": "padding-box, border-box",
      "-webkit-mask-origin": "padding-box, border-box",
      "mask-origin": "padding-box, border-box",
      "-webkit-mask-composite": "xor",
      "mask-composite": "exclude"
    },
    ".bjork-layered-button-accent": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000040",
      "background": "radial-gradient(ellipse at -20px top, rgba(255, 255, 255, 0.14), transparent), linear-gradient(180deg, #ec5c13 0%, #ec5c13 62%, color-mix(in oklab, #ec5c13 84%, black 16%) 100%)"
    },
    ".bjork-layered-button-raised": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, oklch(0.1881 0.006 265), oklch(0.5493 0.0081 265))"
    },
    ".dark .bjork-layered-button-raised": {
      "color": "oklch(0.2574 0.0056 265)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, color-mix(in oklab, oklch(0.9674 0.0013 265) 80%, oklch(0.2574 0.0056 265) 20%), color-mix(in oklab, oklch(0.669 0.0107 265) 80%, oklch(0.2574 0.0056 265) 20%))"
    },
    "@media (hover: hover)": {
      ".bjork-layered-button-accent:hover": {
        "filter": "brightness(1.08) saturate(1.04)"
      },
      ".bjork-layered-button-raised:hover": {
        "filter": "brightness(1.08) contrast(0.9)"
      }
    }
  },
  "meta": {
    "collection": "AI",
    "registryUrl": "https://ui.isaiahbjork.com/memory-viewer.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/memory-viewer.json"
  },
  "type": "registry:component"
}