{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "lib",
  "title": "Lib",
  "description": "cn(), cva, focus ring and --hj-motion-* token readers.",
  "dependencies": [
    "class-variance-authority",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/lib/index.ts",
      "content": "import { type ClassValue, clsx } from 'clsx'\nimport { extendTailwindMerge } from 'tailwind-merge'\n\nexport { cva, cx, type VariantProps } from 'class-variance-authority'\n\n/* -------------------------------------------------------------------------------------------------\n * Class merging\n * -----------------------------------------------------------------------------------------------*/\n\n/** Fluid type scale exposed by `@hjconsult/tokens/theme.css` as `text-*` utilities. */\nexport const FLUID_TEXT_SIZES = [\n  'display-1',\n  'display-2',\n  'h1',\n  'h2',\n  'h3',\n  'h4',\n  'h5',\n  'h6',\n  'lead',\n  'body',\n  'small',\n  'caption',\n] as const\n\n/**\n * `tailwind-merge` taught about the token theme so that custom utilities resolve to the right\n * conflict groups: `text-display-1` is a font size (not a colour), `shadow-glow` is a box shadow\n * (not a shadow colour), `ease-out-expo` is a timing function, `max-w-narrow` is a container width,\n * `rounded-pill` is a radius (9999px in Nordlys, 4px in Systems).\n */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    theme: {\n      text: [...FLUID_TEXT_SIZES],\n      font: ['display', 'body', 'mono', 'label'],\n      shadow: ['1', '2', '3', 'glow', 'hud'],\n      radius: ['pill'],\n      ease: ['standard', 'out-quart', 'out-expo', 'in-out', 'anticipate'],\n      container: ['prose', 'narrow', 'default', 'wide'],\n    },\n  },\n})\n\n/** Merge class names with Tailwind conflict resolution. */\nexport function cn(...inputs: ClassValue[]): string {\n  return twMerge(clsx(inputs))\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Focus ring\n * -----------------------------------------------------------------------------------------------*/\n\n/** Always-visible keyboard focus treatment shared by every interactive component. */\nexport const focusRing =\n  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-2 focus-visible:ring-offset-canvas'\n\n/* -------------------------------------------------------------------------------------------------\n * Motion tokens (read from CSS variables at runtime)\n * -----------------------------------------------------------------------------------------------*/\n\nexport type MotionDuration = 'fast' | 'type' | 'base' | 'slow' | 'hold' | 'hero'\nexport type MotionEase = 'standard' | 'out-quart' | 'out-expo' | 'in-out' | 'anticipate'\nexport type MotionSpring = 'snappy' | 'gentle' | 'bouncy'\n\n/**\n * Fallbacks mirror the defaults published by `@hjconsult/tokens`. They are only used where CSS\n * variables cannot be read: server rendering, tests and the first client frame before styles load.\n */\nconst DURATION_FALLBACK_MS: Record<MotionDuration | 'stagger', number> = {\n  fast: 160,\n  type: 42,\n  base: 240,\n  slow: 480,\n  hold: 2200,\n  hero: 900,\n  stagger: 60,\n}\n\nconst EASE_FALLBACK: Record<MotionEase, string> = {\n  standard: 'cubic-bezier(0.2, 0, 0, 1)',\n  'out-quart': 'cubic-bezier(0.25, 1, 0.5, 1)',\n  'out-expo': 'cubic-bezier(0.16, 1, 0.3, 1)',\n  'in-out': 'cubic-bezier(0.65, 0, 0.35, 1)',\n  anticipate: 'cubic-bezier(0.68, -0.6, 0.32, 1.6)',\n}\n\nconst SPRING_FALLBACK: Record<MotionSpring, { stiffness: number; damping: number; mass: number }> =\n  {\n    snappy: { stiffness: 420, damping: 32, mass: 1 },\n    gentle: { stiffness: 160, damping: 24, mass: 1 },\n    bouncy: { stiffness: 320, damping: 18, mass: 1 },\n  }\n\nconst isBrowser = (): boolean => typeof window !== 'undefined' && typeof document !== 'undefined'\n\n/** Read a raw custom property from `:root`. Empty string when unavailable (SSR, missing token). */\nexport function readCssVar(name: string, element?: Element | null): string {\n  if (!isBrowser()) return ''\n  try {\n    const target = element ?? document.documentElement\n    return getComputedStyle(target).getPropertyValue(name).trim()\n  } catch {\n    return ''\n  }\n}\n\n/** Parse a CSS `<time>` (`240ms`, `.24s`) into milliseconds. `NaN` when it cannot be parsed. */\nexport function parseCssTime(value: string): number {\n  const trimmed = value.trim()\n  if (!trimmed) return Number.NaN\n  const match = /^(-?\\d*\\.?\\d+)\\s*(ms|s)?$/i.exec(trimmed)\n  if (!match) return Number.NaN\n  const amount = Number.parseFloat(match[1] ?? '')\n  const unit = (match[2] ?? 'ms').toLowerCase()\n  return unit === 's' ? amount * 1000 : amount\n}\n\n/**\n * Motion duration token in milliseconds.\n * Reads `--hj-motion-duration-<name>` (or `--hj-motion-stagger-base` for `'stagger'`).\n */\nexport function readMotionToken(name: MotionDuration | 'stagger'): number {\n  const variable = name === 'stagger' ? '--hj-motion-stagger-base' : `--hj-motion-duration-${name}`\n  const parsed = parseCssTime(readCssVar(variable))\n  return Number.isFinite(parsed) ? parsed : DURATION_FALLBACK_MS[name]\n}\n\n/** Motion easing token as a `cubic-bezier(...)` string. Reads `--hj-motion-ease-<name>`. */\nexport function readEase(name: MotionEase): string {\n  const value = readCssVar(`--hj-motion-ease-${name}`)\n  return value.startsWith('cubic-bezier(') ? value : EASE_FALLBACK[name]\n}\n\n/** The four control points of a `cubic-bezier(...)` string; used by GSAP CustomEase and Motion. */\nexport function parseCubicBezier(value: string): [number, number, number, number] | null {\n  const numbers = value.match(/-?\\d*\\.?\\d+(?:e-?\\d+)?/g)?.map(Number)\n  if (numbers?.length !== 4 || numbers.some((n) => !Number.isFinite(n))) return null\n  return [numbers[0] ?? 0, numbers[1] ?? 0, numbers[2] ?? 1, numbers[3] ?? 1]\n}\n\n/** Spring token for the `motion` package. Reads `--hj-motion-spring-<name>-{stiffness,damping,mass}`. */\nexport function readSpring(name: MotionSpring): {\n  stiffness: number\n  damping: number\n  mass: number\n} {\n  const fallback = SPRING_FALLBACK[name]\n  const read = (key: keyof typeof fallback): number => {\n    const parsed = Number.parseFloat(readCssVar(`--hj-motion-spring-${name}-${key}`))\n    return Number.isFinite(parsed) ? parsed : fallback[key]\n  }\n  return { stiffness: read('stiffness'), damping: read('damping'), mass: read('mass') }\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Environment\n * -----------------------------------------------------------------------------------------------*/\n\nexport const COARSE_POINTER_QUERY = '(hover: none) and (pointer: coarse)'\n\n// `prefersReducedMotion` now answers with the visitor's own choice where they made one, and the\n// operating system's everywhere else — see motion-preference.ts. Re-exported from here because\n// every caller already imports it from `@hjconsult/ui/lib`.\nexport {\n  DEFAULT_MOTION_PREFERENCE,\n  getMotionPreference,\n  isMotionPreference,\n  MOTION_EVENT,\n  MOTION_PREF_ATTRIBUTE,\n  MOTION_PREFERENCES,\n  MOTION_STORAGE_KEY,\n  type MotionEventDetail,\n  type MotionPreference,\n  prefersReducedMotion,\n  REDUCED_MOTION_QUERY,\n  setMotionPreference,\n  subscribeMotionPreference,\n  systemPrefersReducedMotion,\n} from './motion-preference'\n\n// Sound is its own switch, off by default, and deliberately not wired to the motion preference —\n// see sound-preference.ts. Re-exported here so callers reach for one import, as with motion.\nexport {\n  DEFAULT_SOUND_PREFERENCE,\n  getSoundPreference,\n  isSoundPreference,\n  SOUND_EVENT,\n  SOUND_PREF_ATTRIBUTE,\n  SOUND_PREFERENCES,\n  SOUND_STORAGE_KEY,\n  type SoundEventDetail,\n  type SoundPreference,\n  setSoundPreference,\n  soundEnabled,\n  subscribeSoundPreference,\n} from './sound-preference'\n\n/** `true` on touch-first devices (no hover, coarse pointer). Always `false` on the server. */\nexport function hasCoarsePointer(): boolean {\n  if (!isBrowser() || typeof window.matchMedia !== 'function') return false\n  try {\n    return window.matchMedia(COARSE_POINTER_QUERY).matches\n  } catch {\n    return false\n  }\n}\n",
      "type": "registry:lib",
      "target": "hj/lib/index.ts"
    }
  ],
  "cssVars": {},
  "type": "registry:lib"
}