{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spec-sheet",
  "title": "Spec sheet",
  "description": "Spec sheet is a production-ready tables component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "lucide-react",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/tables/spec-sheet.tsx",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type KeyboardEvent as ReactKeyboardEvent,\n} from \"react\";\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport { useTheme } from \"next-themes\";\nimport { Check, Minus } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  type BjorkTableThemeMode,\n  getBjorkTablePalette,\n  useBjorkTableIsDark,\n} from \"./table-theme\";\n\nexport interface SpecVariant {\n  id: string;\n  name: string;\n  /** Short line under the name, such as a price. */\n  detail?: string;\n}\n\n/** A cell value. Use `{ value, unit }` when one cell needs a different unit than its row. */\nexport type SpecValue = string | number | boolean | null | { value: number; unit?: string };\n\nexport interface SpecRow {\n  id?: string;\n  label: string;\n  /** One value per variant id. A plain value applies to every variant. */\n  value: Record<string, SpecValue> | SpecValue;\n  unit?: string;\n  note?: string;\n  /** Fraction digits for numeric values. Defaults to what the number needs. */\n  precision?: number;\n}\n\nexport interface SpecGroup {\n  id: string;\n  label: string;\n  rows: SpecRow[];\n}\n\nexport interface SpecSheetProps {\n  groups?: SpecGroup[];\n  /** Up to three variants to compare. Extra variants are ignored. */\n  variants?: SpecVariant[];\n  title?: string;\n  /** Start with only the differing rows visible. */\n  defaultDifferencesOnly?: boolean;\n  /** Controlled \"differences only\" state. */\n  differencesOnly?: boolean;\n  onDifferencesOnlyChange?: (value: boolean) => void;\n  /** Variant shown first in the narrow, one-column layout. */\n  defaultVariantId?: string;\n  onVariantChange?: (variantId: string) => void;\n  /** Max height of the scrolling body. Header and group headings stick. */\n  maxHeight?: number;\n  loading?: boolean;\n  locale?: string;\n  className?: string;\n  theme?: BjorkTableThemeMode;\n  enableAnimations?: boolean;\n}\n\nexport const SPEC_SHEET_SAMPLE_VARIANTS: SpecVariant[] = [\n  { id: \"h14\", name: \"Halden 14\", detail: \"from $1,599\" },\n  { id: \"h16\", name: \"Halden 16\", detail: \"from $2,299\" },\n  { id: \"h16p\", name: \"Halden 16 Pro\", detail: \"from $3,199\" },\n];\n\nexport const SPEC_SHEET_SAMPLE: SpecGroup[] = [\n  {\n    id: \"display\",\n    label: \"Display\",\n    rows: [\n      { label: \"Size\", value: { h14: 14.2, h16: 16.2, h16p: 16.2 }, unit: \"in\", note: \"Diagonal, viewable\" },\n      { label: \"Resolution\", value: { h14: \"2880 × 1800\", h16: \"3456 × 2160\", h16p: \"3456 × 2160\" } },\n      { label: \"Panel\", value: { h14: \"IPS\", h16: \"IPS\", h16p: \"Mini-LED, 2,304 zones\" } },\n      { label: \"Peak brightness\", value: { h14: 500, h16: 600, h16p: 1600 }, unit: \"nits\", note: \"HDR content\" },\n      { label: \"Refresh rate\", value: { h14: 120, h16: 120, h16p: 165 }, unit: \"Hz\" },\n      { label: \"Color gamut\", value: \"100% DCI-P3\" },\n      { label: \"Finish\", value: { h14: \"Glossy\", h16: \"Glossy\", h16p: \"Nano-texture\" } },\n    ],\n  },\n  {\n    id: \"performance\",\n    label: \"Performance\",\n    rows: [\n      { label: \"Processor\", value: { h14: \"H2, 10-core\", h16: \"H2, 12-core\", h16p: \"H2 Max, 16-core\" } },\n      { label: \"Graphics\", value: { h14: 16, h16: 20, h16p: 40 }, unit: \"cores\" },\n      { label: \"Memory\", value: { h14: 16, h16: 32, h16p: 64 }, unit: \"GB\", note: \"Unified, not upgradable\" },\n      {\n        label: \"Storage\",\n        value: { h14: { value: 512, unit: \"GB\" }, h16: { value: 1, unit: \"TB\" }, h16p: { value: 2, unit: \"TB\" } },\n      },\n      { label: \"Neural engine\", value: 38, unit: \"TOPS\" },\n      { label: \"Cooling\", value: { h14: \"Single fan\", h16: \"Dual fan\", h16p: \"Dual fan, vapor chamber\" } },\n    ],\n  },\n  {\n    id: \"battery\",\n    label: \"Battery\",\n    rows: [\n      { label: \"Capacity\", value: { h14: 72, h16: 100, h16p: 100 }, unit: \"Wh\" },\n      { label: \"Video playback\", value: { h14: 19, h16: 22, h16p: 18 }, unit: \"h\", note: \"Local 1080p, 150 nits\" },\n      { label: \"Charger\", value: { h14: 70, h16: 96, h16p: 140 }, unit: \"W\" },\n      { label: \"Fast charge\", value: { h14: false, h16: true, h16p: true }, note: \"50% in 30 min\" },\n    ],\n  },\n  {\n    id: \"connectivity\",\n    label: \"Connectivity\",\n    rows: [\n      { label: \"USB-C / Thunderbolt\", value: { h14: 2, h16: 3, h16p: 3 }, unit: \"ports\" },\n      { label: \"HDMI 2.1\", value: { h14: false, h16: true, h16p: true } },\n      { label: \"SD card slot\", value: { h14: false, h16: true, h16p: true } },\n      { label: \"Wireless\", value: \"Wi-Fi 7, Bluetooth 5.4\" },\n      { label: \"Headphone jack\", value: true },\n      { label: \"Webcam\", value: \"1080p, Center Frame\" },\n    ],\n  },\n  {\n    id: \"dimensions\",\n    label: \"Dimensions\",\n    rows: [\n      { label: \"Width\", value: { h14: 312.6, h16: 355.7, h16p: 355.7 }, unit: \"mm\", precision: 1 },\n      { label: \"Depth\", value: { h14: 221.2, h16: 248.1, h16p: 248.1 }, unit: \"mm\", precision: 1 },\n      { label: \"Thickness\", value: { h14: 14.9, h16: 16.8, h16p: 16.8 }, unit: \"mm\", precision: 1 },\n      { label: \"Enclosure\", value: \"Recycled aluminium\" },\n      { label: \"Weight\", value: { h14: 1.38, h16: 2.04, h16p: 2.16 }, unit: \"kg\", precision: 2 },\n    ],\n  },\n];\n\n// ─── helpers ─────────────────────────────────────────────────────────────────\n\nconst noopSubscribe = () => () => {};\n\nfunction useMounted() {\n  return useSyncExternalStore(noopSubscribe, () => true, () => false);\n}\n\nfunction useIsNarrow<T extends HTMLElement>(threshold: number) {\n  const ref = useRef<T>(null);\n  const [narrow, setNarrow] = useState(false);\n  useEffect(() => {\n    const node = ref.current;\n    if (!node || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver((entries) => {\n      const width = entries[0]?.contentRect.width ?? 0;\n      setNarrow(width > 0 && width < threshold);\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [threshold]);\n  return [ref, narrow] as const;\n}\n\nfunction isPerVariant(value: SpecRow[\"value\"]): value is Record<string, SpecValue> {\n  return typeof value === \"object\" && value !== null && !(\"value\" in value && typeof value.value === \"number\");\n}\n\nfunction cellValue(row: SpecRow, variantId: string): SpecValue {\n  return isPerVariant(row.value)\n    ? ((row.value as Record<string, SpecValue>)[variantId] ?? null)\n    : (row.value as SpecValue);\n}\n\nfunction valueKey(row: SpecRow, value: SpecValue) {\n  if (value !== null && typeof value === \"object\") return `${value.value}${value.unit ?? row.unit ?? \"\"}`;\n  return `${String(value)}${row.unit ?? \"\"}`;\n}\n\nfunction rowDiffers(row: SpecRow, variants: SpecVariant[]) {\n  if (!isPerVariant(row.value) || variants.length < 2) return false;\n  const first = valueKey(row, cellValue(row, variants[0].id));\n  return variants.some((v) => valueKey(row, cellValue(row, v.id)) !== first);\n}\n\nconst formatterCache = new Map<string, Intl.NumberFormat>();\n\nfunction getFormatter(locale: string, precision?: number) {\n  const key = `${locale}|${precision ?? \"\"}`;\n  let formatter = formatterCache.get(key);\n  if (!formatter) {\n    formatter = new Intl.NumberFormat(locale, {\n      minimumFractionDigits: precision ?? 0,\n      maximumFractionDigits: precision ?? 2,\n    });\n    formatterCache.set(key, formatter);\n  }\n  return formatter;\n}\n\n// ─── component ───────────────────────────────────────────────────────────────\n\nconst HEADER_HEIGHT = 58;\n\nexport function SpecSheet({\n  groups = SPEC_SHEET_SAMPLE,\n  variants: variantsProp = SPEC_SHEET_SAMPLE_VARIANTS,\n  title = \"Compare models\",\n  defaultDifferencesOnly = false,\n  differencesOnly: differencesOnlyProp,\n  onDifferencesOnlyChange,\n  defaultVariantId,\n  onVariantChange,\n  maxHeight = 540,\n  loading = false,\n  locale = \"en-US\",\n  className,\n  theme = \"auto\",\n  enableAnimations = true,\n}: SpecSheetProps) {\n  const mounted = useMounted();\n  const { resolvedTheme } = useTheme();\n  const forcedTheme = theme === \"auto\" ? undefined : theme;\n  const detectedIsDark = useBjorkTableIsDark(resolvedTheme, forcedTheme);\n  const isDark = !mounted && theme === \"auto\" ? true : detectedIsDark;\n  const palette = getBjorkTablePalette(isDark);\n  const shouldReduceMotion = useReducedMotion();\n  const shouldAnimate = enableAnimations && !shouldReduceMotion;\n  const uid = useId();\n\n  const variants = useMemo(() => variantsProp.slice(0, 3), [variantsProp]);\n  const [rootRef, narrow] = useIsNarrow<HTMLDivElement>(480);\n  const [diffOnlyState, setDiffOnlyState] = useState(defaultDifferencesOnly);\n  const diffOnly = differencesOnlyProp ?? diffOnlyState;\n  const [pickedId, setPickedId] = useState(defaultVariantId ?? variants[variants.length - 1]?.id);\n  const picked = variants.find((v) => v.id === pickedId) ?? variants[0];\n  const [intro, setIntro] = useState(true);\n  const pickerRefs = useRef(new Map<string, HTMLButtonElement>());\n\n  useEffect(() => {\n    const t = window.setTimeout(() => setIntro(false), 900);\n    return () => window.clearTimeout(t);\n  }, []);\n\n  const formatNumber = (n: number, precision?: number) => getFormatter(locale, precision).format(n);\n\n  const { visibleGroups, diffCount, totalCount } = useMemo(() => {\n    let diff = 0;\n    let total = 0;\n    const out = groups\n      .map((group) => {\n        const rows = group.rows.map((row) => {\n          const differs = rowDiffers(row, variants);\n          total += 1;\n          if (differs) diff += 1;\n          return { row, differs };\n        });\n        return { group, rows: diffOnly ? rows.filter((r) => r.differs) : rows };\n      })\n      .filter((g) => g.rows.length > 0);\n    return { visibleGroups: out, diffCount: diff, totalCount: total };\n  }, [groups, variants, diffOnly]);\n\n  const setDiffOnly = (next: boolean) => {\n    if (differencesOnlyProp === undefined) setDiffOnlyState(next);\n    onDifferencesOnlyChange?.(next);\n  };\n\n  const pick = (id: string, focus = false) => {\n    setPickedId(id);\n    onVariantChange?.(id);\n    if (focus) pickerRefs.current.get(id)?.focus();\n  };\n\n  const handlePickerKey = (event: ReactKeyboardEvent<HTMLButtonElement>, index: number) => {\n    const last = variants.length - 1;\n    let next = -1;\n    if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") next = index === last ? 0 : index + 1;\n    else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") next = index === 0 ? last : index - 1;\n    if (next < 0) return;\n    event.preventDefault();\n    pick(variants[next].id, true);\n  };\n\n  const focusRing = \"outline-none focus-visible:ring-2 focus-visible:ring-[#ec5c13]/60\";\n  const headerLabel = \"text-[11px] font-medium uppercase tracking-[0.08em]\";\n  const bar = isDark ? \"bg-[#d86a2c]\" : \"bg-[#bd4514]\";\n  const surface = isDark ? \"bg-[#111]\" : \"bg-[#fffcf6]\";\n\n  const renderValue = (row: SpecRow, value: SpecValue) => {\n    if (value === null || value === undefined) {\n      return (\n        <span className={palette.secondaryText}>\n          <span aria-hidden>—</span>\n          <span className=\"sr-only\">Not available</span>\n        </span>\n      );\n    }\n    if (typeof value === \"boolean\") {\n      return value ? (\n        <span className={cn(\"inline-flex items-center gap-1.5\", palette.primaryText)}>\n          <Check size={14} strokeWidth={2.4} className={palette.accent} aria-hidden />\n          <span className=\"sr-only\">Yes</span>\n        </span>\n      ) : (\n        <span className={cn(\"inline-flex items-center\", palette.secondaryText)}>\n          <Minus size={14} strokeWidth={2} aria-hidden />\n          <span className=\"sr-only\">No</span>\n        </span>\n      );\n    }\n    const isObj = typeof value === \"object\";\n    const num = isObj ? value.value : typeof value === \"number\" ? value : null;\n    const unit = isObj ? (value.unit ?? row.unit) : row.unit;\n    if (num !== null) {\n      return (\n        <span className=\"whitespace-nowrap tabular-nums\">\n          <span className={cn(\"font-medium\", palette.primaryText)}>{formatNumber(num, row.precision)}</span>\n          {unit ? <span className={cn(\"ml-[0.28em] text-[11.5px]\", palette.secondaryText)}>{unit}</span> : null}\n        </span>\n      );\n    }\n    return (\n      <span className={cn(\"tabular-nums\", palette.primaryText)}>\n        {String(value)}\n        {unit ? <span className={cn(\"ml-[0.28em] text-[11.5px]\", palette.secondaryText)}>{unit}</span> : null}\n      </span>\n    );\n  };\n\n  const shownVariants = narrow && picked ? [picked] : variants;\n  const labelWidth = narrow ? \"46%\" : variants.length === 1 ? \"40%\" : \"28%\";\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\"w-full overflow-hidden rounded-[18px] border text-[13px]\", palette.container, className)}\n    >\n      <div className={cn(\"flex flex-wrap items-center justify-between gap-x-4 gap-y-2.5 px-5 py-3.5\", narrow && \"px-4\")}>\n        <div className=\"min-w-0\">\n          <h3 className={cn(\"text-[15px] font-semibold tracking-[-0.01em]\", palette.primaryText)}>{title}</h3>\n          <p className={cn(\"mt-0.5 text-[12px] tabular-nums\", palette.secondaryText)}>\n            {loading ? \"Loading specs…\" : `${diffCount} of ${totalCount} specs differ`}\n          </p>\n        </div>\n        {variants.length > 1 ? (\n          <label className={cn(\"flex cursor-pointer select-none items-center gap-2.5 text-[12.5px]\", palette.primaryText)}>\n            <span>Show differences only</span>\n            <button\n              type=\"button\"\n              role=\"switch\"\n              aria-checked={diffOnly}\n              onClick={() => setDiffOnly(!diffOnly)}\n              className={cn(\n                \"relative h-[20px] w-[34px] shrink-0 rounded-full border transition-colors duration-200 motion-reduce:transition-none\",\n                diffOnly\n                  ? \"border-[#ec5c13]/40 bg-[#ec5c13]\"\n                  : isDark\n                    ? \"border-[#2a2a2a] bg-[#1d1d1d]\"\n                    : \"border-[#e2d6c3] bg-[#efe7da]\",\n                focusRing\n              )}\n            >\n              <motion.span\n                aria-hidden\n                className={cn(\n                  \"absolute left-[2px] top-[2px] size-[14px] rounded-full\",\n                  diffOnly ? \"bg-[#fffcf6]\" : isDark ? \"bg-[#ededed]/60\" : \"bg-[#fffcf6] shadow-[0_1px_2px_rgba(62,52,38,0.25)]\"\n                )}\n                initial={false}\n                animate={{ x: diffOnly ? 14 : 0 }}\n                transition={shouldAnimate ? { type: \"spring\", stiffness: 600, damping: 34 } : { duration: 0 }}\n              />\n            </button>\n          </label>\n        ) : null}\n      </div>\n\n      {narrow && variants.length > 1 ? (\n        <div className=\"px-4 pb-3\">\n          <div\n            role=\"radiogroup\"\n            aria-label=\"Model\"\n            className={cn(\"grid gap-1 rounded-[10px] border p-[3px]\", palette.divider, palette.mutedSurface)}\n            style={{ gridTemplateColumns: `repeat(${variants.length}, minmax(0, 1fr))` }}\n          >\n            {variants.map((variant, index) => {\n              const selected = variant.id === picked?.id;\n              return (\n                <button\n                  key={variant.id}\n                  ref={(node) => {\n                    if (node) pickerRefs.current.set(variant.id, node);\n                    else pickerRefs.current.delete(variant.id);\n                  }}\n                  type=\"button\"\n                  role=\"radio\"\n                  aria-checked={selected}\n                  tabIndex={selected ? 0 : -1}\n                  onClick={() => pick(variant.id)}\n                  onKeyDown={(e) => handlePickerKey(e, index)}\n                  className={cn(\n                    \"relative truncate rounded-[7px] px-2 py-1.5 text-[12px] font-medium transition-colors motion-reduce:transition-none\",\n                    selected ? palette.primaryText : palette.secondaryText,\n                    focusRing\n                  )}\n                >\n                  {selected ? (\n                    <motion.span\n                      layoutId={shouldAnimate ? `${uid}-pick` : undefined}\n                      transition={{ type: \"spring\", stiffness: 520, damping: 40 }}\n                      className={cn(\n                        \"absolute inset-0 rounded-[7px] border\",\n                        isDark ? \"border-[#2c2c2c] bg-[#222]\" : \"border-[#eadfce] bg-[#fffcf6] shadow-[var(--bjork-shadow-soft)]\"\n                      )}\n                    />\n                  ) : null}\n                  <span className=\"relative\">{variant.name}</span>\n                </button>\n              );\n            })}\n          </div>\n        </div>\n      ) : null}\n\n      <div\n        role=\"region\"\n        aria-label={`${title} specifications`}\n        aria-busy={loading || undefined}\n        tabIndex={0}\n        className={cn(\"overflow-auto overscroll-contain border-t\", palette.divider, focusRing)}\n        style={{ maxHeight }}\n      >\n        <table className={cn(\"w-full table-fixed border-separate border-spacing-0 text-left\", !narrow && \"min-w-[520px]\")}>\n          <caption className=\"sr-only\">\n            {title}: {variants.map((v) => v.name).join(\", \")}\n            {diffOnly ? \" (differences only)\" : \"\"}\n          </caption>\n          <colgroup>\n            <col style={{ width: labelWidth }} />\n            {shownVariants.map((v) => (\n              <col key={v.id} />\n            ))}\n          </colgroup>\n          <thead>\n            <tr>\n              <th\n                scope=\"col\"\n                className={cn(\"sticky top-0 z-20 border-b pl-5 pr-3 align-bottom\", palette.header, headerLabel, narrow && \"pl-4\")}\n                style={{ height: HEADER_HEIGHT }}\n              >\n                <span className=\"block pb-2.5\">Spec</span>\n              </th>\n              {shownVariants.map((variant) => (\n                <th\n                  key={variant.id}\n                  scope=\"col\"\n                  className={cn(\"sticky top-0 z-20 border-b px-3 align-bottom last:pr-5\", palette.header, narrow && \"last:pr-4\")}\n                  style={{ height: HEADER_HEIGHT }}\n                >\n                  <span className=\"block pb-2.5\">\n                    <span className={cn(\"block truncate text-[13px] font-semibold normal-case tracking-[-0.005em]\", palette.primaryText)}>\n                      {variant.name}\n                    </span>\n                    {variant.detail ? (\n                      <span className={cn(\"block truncate text-[11.5px] font-normal tabular-nums\", palette.secondaryText)}>\n                        {variant.detail}\n                      </span>\n                    ) : null}\n                  </span>\n                </th>\n              ))}\n            </tr>\n          </thead>\n          {loading ? (\n            <tbody aria-hidden>\n              {Array.from({ length: 6 }).map((_, i) => (\n                <tr key={i}>\n                  <td className={cn(\"border-b py-3.5 pl-5 pr-3\", palette.divider)}>\n                    <span className={cn(\"block h-3 w-24 animate-pulse rounded motion-reduce:animate-none\", palette.mutedSurface)} />\n                  </td>\n                  {shownVariants.map((v) => (\n                    <td key={v.id} className={cn(\"border-b px-3 py-3.5\", palette.divider)}>\n                      <span className={cn(\"block h-3 w-16 animate-pulse rounded motion-reduce:animate-none\", palette.mutedSurface)} />\n                    </td>\n                  ))}\n                </tr>\n              ))}\n            </tbody>\n          ) : visibleGroups.length === 0 ? (\n            <tbody>\n              <tr>\n                <td colSpan={shownVariants.length + 1} className=\"px-6 py-12 text-center\">\n                  <span className={cn(\"block text-[13px] font-medium\", palette.primaryText)}>No differences</span>\n                  <span className={cn(\"mt-1 block text-[12.5px]\", palette.secondaryText)}>\n                    These models share every listed spec.\n                  </span>\n                </td>\n              </tr>\n            </tbody>\n          ) : (\n            visibleGroups.map(({ group, rows }, groupIndex) => (\n              <tbody key={group.id}>\n                <tr>\n                  <th\n                    scope=\"colgroup\"\n                    colSpan={shownVariants.length + 1}\n                    className={cn(\n                      \"sticky z-10 border-b pb-2 pl-5 pr-3 pt-4 text-left\",\n                      surface,\n                      palette.divider,\n                      narrow && \"pl-4\",\n                      groupIndex === 0 && \"pt-3.5\"\n                    )}\n                    style={{ top: HEADER_HEIGHT }}\n                  >\n                    <span className={cn(headerLabel, palette.accent)}>{group.label}</span>\n                  </th>\n                </tr>\n                {rows.map(({ row, differs }, rowIndex) => {\n                  const showDiff = differs;\n                  const index = groupIndex * 6 + rowIndex;\n                  const lastInGroup = rowIndex === rows.length - 1;\n                  const cell = cn(\"border-b py-3 align-top\", lastInGroup ? palette.divider : isDark ? \"border-[#1c1c1c]\" : \"border-[#f5eee3]\");\n                  return (\n                    <motion.tr\n                      key={row.id ?? row.label}\n                      initial={intro && shouldAnimate ? { opacity: 0, y: 6 } : false}\n                      animate={{ opacity: 1, y: 0 }}\n                      transition={{ duration: 0.32, ease: [0.22, 1, 0.36, 1], delay: intro ? 0.04 + index * 0.018 : 0 }}\n                      className={cn(\"transition-colors duration-150 motion-reduce:transition-none\", palette.row)}\n                    >\n                      <th scope=\"row\" className={cn(cell, \"relative pl-5 pr-3 font-normal\", narrow && \"pl-4\")}>\n                        {showDiff ? (\n                          <span aria-hidden className={cn(\"absolute bottom-2 left-0 top-2 w-[2px] rounded-r-full\", bar)} />\n                        ) : null}\n                        <span className={cn(\"block\", palette.primaryText, !showDiff && \"opacity-80\")}>{row.label}</span>\n                        {row.note ? (\n                          <span className={cn(\"mt-0.5 block text-[11.5px] leading-[1.4]\", palette.secondaryText)}>{row.note}</span>\n                        ) : null}\n                        {showDiff ? <span className=\"sr-only\">(differs between models)</span> : null}\n                      </th>\n                      {!isPerVariant(row.value) && !narrow && shownVariants.length > 1 ? (\n                        <td colSpan={shownVariants.length} className={cn(cell, \"px-3 last:pr-5\")}>\n                          <span className=\"flex items-center gap-3\">\n                            {renderValue(row, row.value)}\n                            <span aria-hidden className={cn(\"h-px flex-1\", isDark ? \"bg-[#232323]\" : \"bg-[#efe6d8]\")} />\n                            <span className={cn(\"text-[11px]\", palette.secondaryText)}>All models</span>\n                          </span>\n                        </td>\n                      ) : (\n                        shownVariants.map((variant) => (\n                          <td key={variant.id} className={cn(cell, \"px-3 last:pr-5\", narrow && \"last:pr-4\")}>\n                            {renderValue(row, cellValue(row, variant.id))}\n                          </td>\n                        ))\n                      )}\n                    </motion.tr>\n                  );\n                })}\n              </tbody>\n            ))\n          )}\n        </table>\n      </div>\n\n      <div\n        className={cn(\n          \"flex items-center gap-2 border-t px-5 py-2.5 text-[11.5px]\",\n          palette.divider,\n          palette.secondaryText,\n          narrow && \"px-4\"\n        )}\n      >\n        <span aria-hidden className={cn(\"h-3 w-[2px] rounded-full\", bar)} />\n        <span>Marked rows differ between models</span>\n        <span aria-live=\"polite\" className=\"sr-only\">\n          {diffOnly ? `Showing ${diffCount} differing specs` : `Showing all ${totalCount} specs`}\n        </span>\n      </div>\n    </div>\n  );\n}\n\nexport default SpecSheet;\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/tables/spec-sheet.tsx"
    },
    {
      "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"
    },
    {
      "path": "components/bjork-ui/tables/table-theme.ts",
      "content": "import { useEffect, useState } from \"react\";\n\nexport type BjorkTableThemeMode = \"light\" | \"dark\" | \"auto\";\n\nexport function useBjorkTableIsDark(\n  resolvedTheme?: string,\n  forcedTheme?: Exclude<BjorkTableThemeMode, \"auto\">\n) {\n  const [documentTheme, setDocumentTheme] = useState<\"light\" | \"dark\" | null>(null);\n  const [previewTheme, setPreviewTheme] = useState<\"light\" | \"dark\" | null>(null);\n  // Server and first client render must agree, otherwise React keeps the\n  // server-rendered (dark) classes when the real theme resolves to light.\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n    const syncDocumentTheme = () => {\n      const root = document.documentElement;\n      const searchParams = new URLSearchParams(window.location.search);\n      const theme = searchParams.get(\"theme\");\n\n      setPreviewTheme(theme === \"light\" || theme === \"dark\" ? theme : null);\n\n      setDocumentTheme(\n        root.classList.contains(\"light\")\n          ? \"light\"\n          : root.classList.contains(\"dark\")\n            ? \"dark\"\n            : null\n      );\n    };\n\n    syncDocumentTheme();\n\n    const observer = new MutationObserver(syncDocumentTheme);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    return () => observer.disconnect();\n  }, []);\n\n  const activeTheme =\n    forcedTheme ?? previewTheme ?? documentTheme ?? (mounted ? resolvedTheme : undefined);\n\n  return activeTheme !== \"light\";\n}\n\nexport function getBjorkTablePalette(isDark: boolean) {\n  return {\n    container: isDark\n      ? \"border-[#1c1c1c] bg-[#121212] shadow-[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      : \"border-[#f5ede2] bg-[#fffcf6] shadow-[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    header: isDark\n      ? \"border-[#232323] bg-[#181818] text-[#ededed]/42\"\n      : \"border-[#f1e8dc] bg-[#fbf7ef] text-[#171717]/60\",\n    row: isDark\n      ? \"border-[#232323] hover:bg-[#181818]/70\"\n      : \"border-[#f1e8dc] hover:bg-[#f8f2e7]/72\",\n    selectedRow: isDark\n      ? \"border-[#2a2a2a] bg-[#181818]\"\n      : \"border-[#eadfce] bg-[#f8f2e7]\",\n    control: isDark\n      ? \"border-[#232323] bg-[#181818] text-[#ededed]/70 hover:bg-[#202020] shadow-[inset_0_1px_0_rgba(255,255,255,0.045)]\"\n      : \"border-[#eadfce] bg-[#fffcf6] text-[#171717]/68 shadow-[var(--bjork-shadow-soft)] hover:bg-[#f8f2e7]\",\n    menu: isDark\n      ? \"border-[#232323] bg-[#181818] text-[#ededed]/76 shadow-[inset_0_1px_0_rgba(255,255,255,0.055),inset_0_12px_24px_rgba(255,255,255,0.018),0_22px_42px_-22px_rgba(0,0,0,0.9)]\"\n      : \"border-[#eadfce] bg-[#fffcf6] text-[#171717]/76 shadow-[inset_0_1px_0_rgba(88,72,49,0.04),inset_0_12px_24px_rgba(88,72,49,0.022),0_22px_44px_-26px_rgba(66,52,33,0.22)]\",\n    menuItem: isDark ? \"hover:bg-[#232323]\" : \"hover:bg-[#f8f2e7]\",\n    menuActive: isDark ? \"bg-[#232323]\" : \"bg-[#f8f2e7]\",\n    divider: isDark ? \"border-[#232323]\" : \"border-[#f1e8dc]\",\n    primaryText: isDark ? \"text-[#ededed]/90\" : \"text-[#171717]/86\",\n    secondaryText: isDark ? \"text-[#ededed]/38\" : \"text-[#171717]/60\",\n    mutedSurface: isDark ? \"bg-[#181818]\" : \"bg-[#f8f2e7]\",\n    popover: isDark ? \"bg-[#111] text-[#ededed]\" : \"bg-[#fffcf6] text-[#171717]\",\n    accent: isDark ? \"text-[#d86a2c]\" : \"text-[#bd4514]\",\n    accentBg: isDark ? \"bg-[#ec5c13]/12\" : \"bg-[#ec5c13]/14\",\n    accentBorder: isDark ? \"border-[#ec5c13]/24\" : \"border-[#bd4514]/22\",\n    checkboxAccent: isDark ? \"#d86a2c\" : \"#bd4514\",\n  };\n}\n\nexport function getBjorkSignalPalette(\n  tone: \"green\" | \"red\" | \"orange\" | \"neutral\",\n  isDark: boolean\n) {\n  const palettes = {\n    green: {\n      bgColor: isDark ? \"bg-[#3e6f52]/18\" : \"bg-[#dce8dc]\",\n      borderColor: isDark ? \"border-[#5f9f78]/28\" : \"border-[#bad0bd]\",\n      textColor: isDark ? \"text-[#77b58b]\" : \"text-[#47785c]\",\n      dotColor: isDark ? \"bg-[#77b58b]\" : \"bg-[#47785c]\",\n      color: isDark ? \"#77b58b\" : \"#47785c\",\n    },\n    red: {\n      bgColor: isDark ? \"bg-[#87463f]/18\" : \"bg-[#eadad5]\",\n      borderColor: isDark ? \"border-[#c46a61]/28\" : \"border-[#d7bcb4]\",\n      textColor: isDark ? \"text-[#cf7c72]\" : \"text-[#9d5149]\",\n      dotColor: isDark ? \"bg-[#cf7c72]\" : \"bg-[#9d5149]\",\n      color: isDark ? \"#cf7c72\" : \"#9d5149\",\n    },\n    orange: {\n      bgColor: isDark ? \"bg-[#ec5c13]/14\" : \"bg-[#ead9cb]\",\n      borderColor: isDark ? \"border-[#ec5c13]/28\" : \"border-[#d5b59f]\",\n      textColor: isDark ? \"text-[#d86a2c]\" : \"text-[#9f4317]\",\n      dotColor: isDark ? \"bg-[#d86a2c]\" : \"bg-[#9f4317]\",\n      color: isDark ? \"#d86a2c\" : \"#9f4317\",\n    },\n    neutral: {\n      bgColor: isDark ? \"bg-[#ededed]/8\" : \"bg-[#ded7ca]\",\n      borderColor: isDark ? \"border-[#ededed]/14\" : \"border-[#cfc6b7]\",\n      textColor: isDark ? \"text-[#ededed]/58\" : \"text-[#171717]/60\",\n      dotColor: isDark ? \"bg-[#ededed]/45\" : \"bg-[#171717]/42\",\n      color: isDark ? \"#a3a3a3\" : \"#706b62\",\n    },\n  } as const;\n\n  return palettes[tone];\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/tables/table-theme.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": "Tables",
    "registryUrl": "https://ui.isaiahbjork.com/spec-sheet.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/spec-sheet.json"
  },
  "type": "registry:component"
}