{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "citation-chips",
  "title": "Citation chips",
  "description": "Citation chips 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/citation-chips.tsx",
      "content": "\"use client\";\n\nimport {\n  createContext,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport { motion } from \"framer-motion\";\nimport { ArrowUpRight } from \"lucide-react\";\nimport { springs } from \"@/components/bjork-ui/_core/motion\";\nimport { FOCUS_RING, PRESS, useAiTone, useReduceMotion, type BjorkTone } from \"@/components/bjork-ui/ai/_shared\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface CitationSource {\n  id: string;\n  title: string;\n  url: string;\n  /** Shown in mono, such as \"fieldnotes.dev\". */\n  domain: string;\n  snippet: string;\n  /** A passage inside `snippet` that the answer relies on. Highlighted in the preview. */\n  quote?: string;\n  /** ISO date, such as \"2026-03-14\". */\n  publishedAt?: string;\n}\n\n/** Plain text, or a citation of one or more sources by id. */\nexport type CitationSegment = string | { cite: string | string[] };\n\nexport interface CitedTextProps {\n  sources: CitationSource[];\n  /** Text with numbered markers such as \"[1]\" or \"[2][3]\", numbered from 1 in `sources` order. */\n  text?: string;\n  /** Pre-split content. Used instead of `text` when given. */\n  segments?: CitationSegment[];\n  onOpenSource?: (source: CitationSource) => void;\n  /** Demo/preview only: opens the card of this marker (0-based, in reading order) on mount. */\n  defaultOpenIndex?: number;\n  tone?: BjorkTone;\n  className?: string;\n}\n\nexport interface SourceListProps {\n  sources: CitationSource[];\n  /** Chips shown before \"+N more\". Default 4. */\n  visibleCount?: number;\n  onOpenSource?: (source: CitationSource) => void;\n  /** Heading above the chips. Default \"Sources\". Pass null to hide it. */\n  label?: string | null;\n  tone?: BjorkTone;\n  className?: string;\n}\n\nexport const SAMPLE_SOURCES: CitationSource[] = [\n  {\n    id: \"s1\",\n    title: \"Why cities stay hot after dark\",\n    url: \"https://fieldnotes.dev/notes/urban-heat-at-night\",\n    domain: \"fieldnotes.dev\",\n    snippet:\n      \"Most people picture the heat island at noon. The measurements say otherwise: asphalt, brick and concrete bank heat all day and give it back slowly overnight, so the gap with the countryside peaks a few hours after sunset.\",\n    quote: \"the gap with the countryside peaks a few hours after sunset\",\n    publishedAt: \"2026-06-02\",\n  },\n  {\n    id: \"s2\",\n    title: \"Street canyons and the sky view factor\",\n    url: \"https://quarrylab.org/papers/sky-view-factor\",\n    domain: \"quarrylab.org\",\n    snippet:\n      \"In narrow streets lined with tall buildings, surfaces see less open sky. Less of their stored heat radiates upward, and more is exchanged between facing walls, which keeps the canyon warm long into the night.\",\n    quote: \"Less of their stored heat radiates upward\",\n    publishedAt: \"2025-11-19\",\n  },\n  {\n    id: \"s3\",\n    title: \"Canopy versus cool roofs: a street-level comparison\",\n    url: \"https://civic-ledger.net/reports/canopy-cool-roofs\",\n    domain: \"civic-ledger.net\",\n    snippet:\n      \"Reflective roofs lower rooftop temperatures sharply, but pedestrians feel the canopy more. Blocks with mature street trees ran up to 2 °C cooler at night than matched blocks without them.\",\n    quote: \"up to 2 °C cooler at night\",\n    publishedAt: \"2026-04-27\",\n  },\n  {\n    id: \"s4\",\n    title: \"Warm nights and heat-wave health risk\",\n    url: \"https://tidewaterpost.com/health/warm-nights\",\n    domain: \"tidewaterpost.com\",\n    snippet:\n      \"Hospital admissions track night-time lows more closely than daytime highs. When nights stay warm, the body never gets the recovery window it needs between hot days.\",\n    quote: \"the body never gets the recovery window it needs\",\n    publishedAt: \"2026-07-30\",\n  },\n  {\n    id: \"s5\",\n    title: \"Mapping a city's thermal footprint by bicycle\",\n    url: \"https://openharbor.io/blog/thermal-bike-map\",\n    domain: \"openharbor.io\",\n    snippet: \"Volunteers rode fixed routes at 9pm with clip-on sensors, producing a block-by-block night temperature map.\",\n    publishedAt: \"2025-08-12\",\n  },\n  {\n    id: \"s6\",\n    title: \"Materials that release heat faster\",\n    url: \"https://lowtide.science/articles/emissive-pavements\",\n    domain: \"lowtide.science\",\n    snippet: \"High-emissivity coatings let pavements shed stored heat earlier in the evening.\",\n    publishedAt: \"2026-01-08\",\n  },\n  {\n    id: \"s7\",\n    title: \"A short history of the urban heat island\",\n    url: \"https://marginalia.press/essays/heat-island-history\",\n    domain: \"marginalia.press\",\n    snippet: \"The effect was first described in 1818, from thermometer readings taken inside and outside London.\",\n    publishedAt: \"2024-12-03\",\n  },\n];\n\nexport const SAMPLE_CITED_ANSWER =\n  \"Urban heat islands are strongest after sunset, not at noon: asphalt and brick store heat all day and release it slowly overnight [1]. Dense blocks trap that warmth too, because narrow streets limit how much heat can radiate back to the sky [2]. At street level, trees do more than reflective roofs, cutting night-time temperatures by up to 2 °C [3]. The effect compounds in heat waves, when warm nights leave no time to recover [4].\";\n\nconst OPEN_DELAY = 150;\nconst EDGE = 12;\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n\nfunction formatDate(iso?: string): string {\n  if (!iso) return \"\";\n  const m = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(iso);\n  if (!m) return iso;\n  return `${MONTHS[Number(m[2]) - 1]} ${Number(m[3])}, ${m[1]}`;\n}\n\n// Deterministic favicon tile: the same domain always gets the same tone.\nconst TILE_TONES = [\n  \"bg-[color:var(--bjork-accent-soft)] text-[color:var(--bjork-accent-ink)]\",\n  \"bg-[color:var(--bjork-surface-active)] text-[color:var(--bjork-text-medium)]\",\n  \"bg-[color:var(--bjork-text)] text-[color:var(--bjork-ring-offset)]\",\n  \"bg-[color:var(--bjork-hair)] text-[color:var(--bjork-text)]\",\n];\n\nfunction hash(s: string): number {\n  return s.split(\"\").reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7);\n}\n\nfunction FaviconTile({ domain, size = 16 }: { domain: string; size?: number }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"grid shrink-0 place-items-center rounded-[5px] font-mono font-semibold uppercase leading-none\",\n        TILE_TONES[hash(domain) % TILE_TONES.length],\n      )}\n      style={{ width: size, height: size, fontSize: Math.max(10, Math.round(size * 0.56)) }}\n    >\n      {domain.replace(/^www\\./, \"\")[0]}\n    </span>\n  );\n}\n\n/* Shared highlight between inline markers and source chips. */\n\ninterface CitationCtx {\n  hot: string | null;\n  setHot: (id: string | null) => void;\n}\nconst Ctx = createContext<CitationCtx | null>(null);\n\n/** Wrap a CitedText and SourceList so hovering either highlights the matching chips in the other. */\nexport function CitationGroup({ children, defaultHighlighted = null }: { children: ReactNode; defaultHighlighted?: string | null }) {\n  const [hot, setHot] = useState<string | null>(defaultHighlighted);\n  const value = useMemo(() => ({ hot, setHot }), [hot]);\n  return <Ctx.Provider value={value}>{children}</Ctx.Provider>;\n}\n\nfunction useHot(): CitationCtx {\n  const shared = useContext(Ctx);\n  const [hot, setHot] = useState<string | null>(null);\n  return shared ?? { hot, setHot };\n}\n\n/* Inline text with markers. */\n\nfunction parse(text: string, sources: CitationSource[]): CitationSegment[] {\n  const parts = text.split(/((?:\\s*\\[\\d+\\])+)/g).filter((p) => p !== \"\");\n  return parts.map((p) => {\n    const nums = [...p.matchAll(/\\[(\\d+)\\]/g)].map((m) => Number(m[1]));\n    if (nums.length === 0) return p;\n    const ids = nums.map((n) => sources[n - 1]?.id).filter((id): id is string => Boolean(id));\n    return { cite: ids };\n  });\n}\n\n/**\n * Answer text with numbered citation chips. Each chip previews its source on hover or focus, and highlights the\n * matching chip in a SourceList inside the same CitationGroup.\n */\nexport function CitedText({\n  sources,\n  text = \"\",\n  segments,\n  onOpenSource,\n  defaultOpenIndex,\n  tone: toneProp,\n  className,\n}: CitedTextProps) {\n  const { style } = useAiTone(toneProp);\n  const { hot, setHot } = useHot();\n  const content = useMemo(() => segments ?? parse(text, sources), [segments, text, sources]);\n  const indexOf = (id: string) => sources.findIndex((s) => s.id === id);\n\n  // Marker order in reading order, so `defaultOpenIndex` can address one.\n  const markers = content.flatMap((seg, si) =>\n    typeof seg === \"string\" ? [] : [seg.cite].flat().map((id, ci) => ({ key: `${si}-${ci}`, id })),\n  );\n\n  return (\n    <p className={cn(\"font-bjork-alpha text-[15px] leading-[26px] text-[color:var(--bjork-text-medium)]\", className)} style={style}>\n      {content.map((seg, si) => {\n        if (typeof seg === \"string\") return <span key={si}>{seg}</span>;\n        const ids = [seg.cite].flat();\n        return (\n          <span key={si} className=\"whitespace-nowrap\">\n            {ids.map((id, ci) => {\n              const source = sources.find((s) => s.id === id);\n              if (!source) return null;\n              const key = `${si}-${ci}`;\n              const order = markers.findIndex((m) => m.key === key);\n              return (\n                <CitationChip\n                  key={key}\n                  source={source}\n                  inline\n                  number={indexOf(id) + 1}\n                  hot={hot === id}\n                  setHot={setHot}\n                  onOpenSource={onOpenSource}\n                  defaultOpen={order === defaultOpenIndex}\n                />\n              );\n            })}\n          </span>\n        );\n      })}\n    </p>\n  );\n}\n\n/** Source chips under an answer, with \"+N more\" for the rest. */\nexport function SourceList({\n  sources,\n  visibleCount = 4,\n  onOpenSource,\n  label = \"Sources\",\n  tone: toneProp,\n  className,\n}: SourceListProps) {\n  const { style } = useAiTone(toneProp);\n  const reduce = useReduceMotion();\n  const { hot, setHot } = useHot();\n  const [expanded, setExpanded] = useState(false);\n  const headingId = useId();\n  const shown = expanded ? sources : sources.slice(0, visibleCount);\n  const hidden = sources.length - shown.length;\n\n  return (\n    <section\n      aria-labelledby={label ? headingId : undefined}\n      aria-label={label ? undefined : \"Sources\"}\n      className={cn(\"font-bjork-alpha text-[color:var(--bjork-text)]\", className)}\n      style={style}\n    >\n      {label && (\n        <h3 id={headingId} className=\"mb-2 font-mono text-[10px] font-normal uppercase tracking-[0.08em] text-[color:var(--bjork-text-faint)]\">\n          {label} <span className=\"tabular-nums\">· {sources.length}</span>\n        </h3>\n      )}\n      <ul className=\"flex flex-wrap gap-1.5\">\n        {shown.map((source, i) => (\n          <motion.li\n            key={source.id}\n            // The blur-in leaves a filter, so every item is its own stacking context. The item with an open card is\n            // lifted above its later siblings.\n            className={cn(\n              \"relative min-w-0 max-w-full hover:z-20 focus-within:z-20\",\n              hot === source.id && \"z-20\",\n            )}\n            initial={i >= visibleCount && !reduce ? { opacity: 0, y: 4, filter: \"blur(4px)\" } : false}\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            transition={reduce ? { duration: 0 } : { ...springs.blurIn, delay: Math.max(0, i - visibleCount) * 0.03 }}\n          >\n            <CitationChip\n              source={source}\n              number={i + 1}\n              hot={hot === source.id}\n              setHot={setHot}\n              onOpenSource={onOpenSource}\n            />\n          </motion.li>\n        ))}\n        {sources.length > visibleCount && (\n          <li>\n            <button\n              type=\"button\"\n              aria-expanded={expanded}\n              onClick={() => setExpanded((v) => !v)}\n              className={cn(\n                \"h-8 cursor-pointer rounded-full px-3 font-mono text-[11px] tabular-nums 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              {expanded ? \"Show less\" : `+${hidden} more`}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n\n/* One chip and its preview card. */\n\ninterface ChipProps {\n  source: CitationSource;\n  number: number;\n  inline?: boolean;\n  hot: boolean;\n  setHot: (id: string | null) => void;\n  onOpenSource?: (source: CitationSource) => void;\n  defaultOpen?: boolean;\n}\n\ntype OpenReason = \"hover\" | \"focus\" | \"press\";\n\nfunction CitationChip({ source, number, inline = false, hot, setHot, onOpenSource, defaultOpen = false }: ChipProps) {\n  const [openBy, setOpenBy] = useState<OpenReason | null>(defaultOpen ? \"press\" : null);\n  const open = openBy !== null;\n  const wrapRef = useRef<HTMLSpanElement>(null);\n  const cardRef = useRef<HTMLSpanElement>(null);\n  const timer = useRef<number | undefined>(undefined);\n  const cardId = useId();\n\n  const cancel = () => window.clearTimeout(timer.current);\n  const openLater = (reason: OpenReason) => {\n    cancel();\n    timer.current = window.setTimeout(() => setOpenBy((cur) => cur ?? reason), OPEN_DELAY);\n  };\n  useEffect(() => () => window.clearTimeout(timer.current), []);\n\n  // Escape closes; a press outside closes a pressed card (touch).\n  useEffect(() => {\n    if (!open) return;\n    const shut = () => {\n      window.clearTimeout(timer.current);\n      setOpenBy(null);\n    };\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") shut();\n    };\n    const onDown = (e: PointerEvent) => {\n      if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) shut();\n    };\n    document.addEventListener(\"keydown\", onKey);\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => {\n      document.removeEventListener(\"keydown\", onKey);\n      document.removeEventListener(\"pointerdown\", onDown);\n    };\n  }, [open]);\n\n  // Flip above or below by available room, then keep the card inside the viewport horizontally.\n  useLayoutEffect(() => {\n    const wrap = wrapRef.current;\n    const card = cardRef.current;\n    if (!open || !wrap || !card) return;\n    const place = () => {\n      const r = wrap.getBoundingClientRect();\n      const h = card.offsetHeight;\n      const w = card.offsetWidth;\n      const below = window.innerHeight - r.bottom;\n      const placeBelow = below >= h || below >= r.top;\n      card.style.top = placeBelow ? \"100%\" : \"auto\";\n      card.style.bottom = placeBelow ? \"auto\" : \"100%\";\n      card.dataset.side = placeBelow ? \"bottom\" : \"top\";\n      const want = r.left + r.width / 2 - w / 2;\n      const left = Math.min(Math.max(EDGE, want), Math.max(EDGE, window.innerWidth - EDGE - w));\n      card.style.left = `${left - r.left}px`;\n    };\n    place();\n    window.addEventListener(\"resize\", place);\n    window.addEventListener(\"scroll\", place, true);\n    return () => {\n      window.removeEventListener(\"resize\", place);\n      window.removeEventListener(\"scroll\", place, true);\n    };\n  }, [open]);\n\n  const isMouse = (e: ReactPointerEvent) => e.pointerType === \"mouse\" || e.pointerType === \"pen\";\n\n  return (\n    <span\n      ref={wrapRef}\n      className={cn(\"relative\", inline ? \"inline-block align-baseline\" : \"inline-flex max-w-full\")}\n      onPointerEnter={(e) => {\n        if (!isMouse(e)) return;\n        setHot(source.id);\n        openLater(\"hover\");\n      }}\n      onPointerLeave={(e) => {\n        if (!isMouse(e)) return;\n        setHot(null);\n        cancel();\n        // Instant out, unless keyboard or a press opened it.\n        setOpenBy((cur) => (cur === \"hover\" ? null : cur));\n      }}\n      onBlur={(e) => {\n        if (wrapRef.current?.contains(e.relatedTarget as Node)) return;\n        setHot(null);\n        cancel();\n        setOpenBy((cur) => (cur === \"hover\" ? cur : null));\n      }}\n    >\n      <button\n        type=\"button\"\n        aria-describedby={cardId}\n        aria-expanded={open}\n        aria-label={inline ? `Source ${number}: ${source.title}` : undefined}\n        onFocus={(e) => {\n          setHot(source.id);\n          if (e.currentTarget.matches(\":focus-visible\")) openLater(\"focus\");\n        }}\n        onClick={() => {\n          cancel();\n          setOpenBy((cur) => (cur === null || cur === \"hover\" ? \"press\" : null));\n        }}\n        className={cn(\n          \"cursor-pointer transition-[background-color,color,border-color] duration-150\",\n          FOCUS_RING,\n          inline\n            ? cn(\n                // 16px chip with a 28px hit area from the pseudo-element. ml nudges it off the preceding word.\n                \"relative ml-[3px] inline-flex h-4 min-w-4 -translate-y-[1px] items-center justify-center rounded-[5px] px-[3px] align-baseline font-mono text-[11px] font-medium leading-none tabular-nums before:absolute before:-inset-x-1.5 before:-inset-y-1.5 before:content-['']\",\n                hot || open\n                  ? \"bg-[color:var(--bjork-accent)] text-[color:var(--bjork-accent-foreground)]\"\n                  : \"bg-[color:var(--bjork-surface-active)] text-[color:var(--bjork-text-muted)] hover:text-[color:var(--bjork-text)]\",\n              )\n            : cn(\n                \"flex h-8 min-w-0 max-w-full items-center gap-2 rounded-full border pl-1.5 pr-3 text-left\",\n                PRESS,\n                hot || open\n                  ? \"border-[color:var(--bjork-accent-muted)] bg-[color:var(--bjork-accent-soft)]\"\n                  : \"border-[color:var(--bjork-border)] hover:bg-[color:var(--bjork-surface-active)]\",\n              ),\n        )}\n      >\n        {inline ? (\n          number\n        ) : (\n          <>\n            <span className=\"w-3.5 shrink-0 text-center font-mono text-[11px] tabular-nums text-[color:var(--bjork-text-faint)]\">{number}</span>\n            <FaviconTile domain={source.domain} />\n            <span className=\"max-w-[148px] truncate text-[12.5px] leading-4 text-[color:var(--bjork-text)]\">{source.title}</span>\n            <span className=\"hidden shrink-0 font-mono text-[11px] text-[color:var(--bjork-text-faint)] min-[440px]:inline\">\n              {source.domain}\n            </span>\n          </>\n        )}\n      </button>\n\n      <PreviewCard ref={cardRef} id={cardId} source={source} open={open} onOpenSource={onOpenSource} />\n    </span>\n  );\n}\n\nfunction highlight(snippet: string, quote?: string): ReactNode {\n  const at = quote ? snippet.indexOf(quote) : -1;\n  if (!quote || at < 0) return snippet;\n  return (\n    <>\n      {snippet.slice(0, at)}\n      <mark className=\"rounded-[3px] bg-[color:var(--bjork-accent-soft)] px-0.5 text-[color:var(--bjork-text)] [box-decoration-break:clone]\">\n        {quote}\n      </mark>\n      {snippet.slice(at + quote.length)}\n    </>\n  );\n}\n\nfunction PreviewCard({\n  ref,\n  id,\n  source,\n  open,\n  onOpenSource,\n}: {\n  ref: Ref<HTMLSpanElement>;\n  id: string;\n  source: CitationSource;\n  open: boolean;\n  onOpenSource?: (source: CitationSource) => void;\n}) {\n  const reduce = useReduceMotion();\n  return (\n    <span\n      ref={ref}\n      id={id}\n      aria-hidden={!open || undefined}\n      data-side=\"bottom\"\n      // The transparent padding bridges the gap, so the pointer can travel from chip to card.\n      className={cn(\n        \"absolute left-0 top-full z-40 w-[min(312px,calc(100vw-24px))] py-1.5 text-left font-bjork-alpha\",\n        open ? \"block\" : \"hidden\",\n      )}\n    >\n      <span\n        className={cn(\n          \"block rounded-[12px] border border-[color:var(--bjork-border)] bg-[color:var(--bjork-menu)] p-3 shadow-[var(--bjork-shadow-menu)] backdrop-blur-md\",\n          !reduce && \"motion-safe:animate-[bjork-cite-in_140ms_cubic-bezier(0.23,1,0.32,1)]\",\n        )}\n      >\n        <style href=\"bjork-citation-card\" precedence=\"default\">\n          {\"@keyframes bjork-cite-in{from{opacity:0;transform:translateY(3px) scale(0.98)}to{opacity:1;transform:none}}\"}\n        </style>\n        <span className=\"flex items-center gap-2\">\n          <FaviconTile domain={source.domain} size={18} />\n          <span className=\"min-w-0 flex-1 truncate font-mono text-[11px] leading-4 text-[color:var(--bjork-text-muted)]\">\n            {source.domain}\n          </span>\n          {source.publishedAt && (\n            <span className=\"shrink-0 font-mono text-[11px] leading-4 tabular-nums text-[color:var(--bjork-text-faint)]\">\n              {formatDate(source.publishedAt)}\n            </span>\n          )}\n        </span>\n        <span className=\"mt-2 line-clamp-2 block text-[14px] font-medium leading-5 text-[color:var(--bjork-text)]\">\n          {source.title}\n        </span>\n        <span className=\"mt-1.5 line-clamp-4 block text-[12.5px] leading-[19px] text-[color:var(--bjork-text-muted)]\">\n          {highlight(source.snippet, source.quote)}\n        </span>\n        <span className=\"mt-2.5 flex border-t border-[color:var(--bjork-border)] pt-2\">\n          <a\n            href={source.url}\n            target=\"_blank\"\n            rel=\"noreferrer noopener\"\n            onClick={(e) => {\n              if (!onOpenSource) return;\n              e.preventDefault();\n              onOpenSource(source);\n            }}\n            className={cn(\n              \"-ml-1 inline-flex h-7 items-center gap-1 rounded-[6px] px-1 text-[12.5px] font-medium text-[color:var(--bjork-accent-ink)] hover:underline hover:underline-offset-2\",\n              FOCUS_RING,\n            )}\n          >\n            Open source\n            <ArrowUpRight aria-hidden=\"true\" size={13} strokeWidth={1.75} />\n          </a>\n        </span>\n      </span>\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/ai/citation-chips.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/citation-chips.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/citation-chips.json"
  },
  "type": "registry:component"
}