{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cn",
  "title": "cn and style primitives",
  "description": "ikada's class helper (clsx plus tailwind-merge that understands the text-sb-* sizes) and the shared style primitives: focus ring, press motion, popup motion and field surfaces. Installs to components/ikada/lib, never to lib/utils.",
  "dependencies": [
    "clsx@^2.1.1",
    "tailwind-merge@^3.6.0"
  ],
  "registryDependencies": [
    "https://sproutboat.com/ikada/r/tokens.json"
  ],
  "files": [
    {
      "path": "registry/ikada/lib/ikada/cn.ts",
      "content": "/**\n * Class helpers and the shared Afloat primitives: focus ring and wake, press settle and bob,\n * popup motion, field and floating surfaces, the recessed tray and the sliding pebble.\n */\nimport { clsx } from \"clsx\";\nimport type { ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\nimport { typeScale } from \"@/components/ikada/lib/tokens\";\n\n/** tailwind-merge that knows our `text-sb-<step>` sizes (and the plain 0.1.x `text-<step>` ones) are sizes, not colors. */\nconst steps = Object.keys(typeScale);\nexport const merge = extendTailwindMerge({\n  extend: { classGroups: { \"font-size\": [{ text: [...steps.map((k) => \"sb-\" + k), ...steps] }] } },\n});\n\n/** Join and de-conflict class names; later arguments win. */\nexport const cn = (...inputs: ClassValue[]): string => merge(clsx(inputs));\n\ntype Variants = Record<string, Record<string, ClassValue>>;\n\n/** Props a `tv` style function takes: one key per variant group plus `className`, which wins. */\nexport type TvProps<V extends Variants> = {\n  [K in keyof V]?: (keyof V[K] & string) | null | undefined;\n} & { className?: ClassValue };\n\n/** The variant props of a style function made by `tv`, without `className`: `VariantProps<typeof buttonStyles>`. */\nexport type VariantProps<F> = F extends (props?: infer P) => string ? Omit<NonNullable<P>, \"className\"> : never;\n\n/**\n * A small class-variance helper (the part of tailwind-variants ikada used, without its bundled\n * tailwind-merge): `base` plus one class set per chosen variant, merged so `className` wins.\n */\nexport function tv<const V extends Variants>(config: {\n  base?: ClassValue;\n  variants: V;\n  defaultVariants?: { [K in keyof V]?: keyof V[K] & string };\n}): (props?: TvProps<V>) => string {\n  return (props = {}) =>\n    cn(\n      config.base,\n      Object.keys(config.variants).map((group) => {\n        const chosen = (props as Record<string, unknown>)[group] ?? config.defaultVariants?.[group];\n        return chosen == null ? null : config.variants[group]?.[String(chosen)];\n      }),\n      props.className,\n    );\n}\n\n/**\n * The focus wake: a halo that ripples 6px out from the ring once, like a wake behind a raft.\n * It rides in Tailwind's ring slot of box-shadow, so it composes with the control's own shadow,\n * and `--sb-wake` / `--sb-wake-alpha` are registered properties animated by `sb-wake` (theme.css).\n * Under reduced motion the halo fades in place without spreading.\n */\nexport const focusWake = [\n  \"focus-visible:animate-[sb-wake_var(--sb-duration-wake)_var(--ease-out)]\",\n  \"focus-visible:ring-(length:--sb-wake) focus-visible:ring-[color-mix(in_oklch,var(--sb-focus)_var(--sb-wake-alpha),transparent)]\",\n].join(\" \");\n\n/** Visible focus ring: 2px neutral (near-black or near-white) outline, keyboard focus only, and its wake. */\nexport const focusRing = [\n  \"outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n  focusWake,\n].join(\" \");\n\n/** Same ring, drawn inside the box, for controls clipped by a scroller. */\nexport const focusRingInset =\n  \"outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus\";\n\n/**\n * Enter and exit for menus, selects, popovers and tooltips. They arrive from scale 0.96 at the\n * trigger (Base UI sets --transform-origin) on the snappy spring, fading in over 140ms, and leave\n * in 110ms ease-out. Keyboard-opened layers carry data-sb-instant and skip the animation.\n */\nexport const popupMotion = [\n  \"origin-(--transform-origin) [transition:opacity_var(--sb-duration-popup-out)_var(--ease-out),scale_var(--sb-duration-popup-out)_var(--ease-out)]\",\n  \"data-open:[transition:opacity_var(--sb-duration-popup-in)_var(--ease-out),scale_var(--sb-duration-spring-snappy)_var(--sb-spring-snappy)]\",\n  \"data-starting-style:scale-(--sb-scale-popup) data-starting-style:opacity-0\",\n  \"data-ending-style:scale-(--sb-scale-popup) data-ending-style:opacity-0\",\n  \"data-instant:transition-none data-sb-instant:transition-none\",\n].join(\" \");\n\n/** Floating surface used by menus, selects and popovers: a lighter step with a layered shadow. */\nexport const floatingSurface = \"rounded-xl bg-raised text-fg shadow-float outline-none\";\n\n/**\n * Afloat press: the control takes your weight. Pressing settles it 1px down and 3% smaller in\n * 90ms ease-out; letting go returns it on the bob spring, which overshoots once. Hover colour and\n * the cast-shadow lift fade over 120ms. Names every property, never \"all\".\n */\nexport const pressMotion = [\n  \"[transition:background-color_var(--sb-duration-hover)_var(--ease-hover),color_var(--sb-duration-hover)_var(--ease-hover),box-shadow_var(--sb-duration-hover)_var(--ease-hover),scale_var(--sb-duration-spring-bob)_var(--sb-spring-bob),translate_var(--sb-duration-spring-bob)_var(--sb-spring-bob)]\",\n  \"active:[transition:background-color_var(--sb-duration-hover)_var(--ease-hover),color_var(--sb-duration-hover)_var(--ease-hover),box-shadow_var(--sb-duration-press)_var(--ease-out),scale_var(--sb-duration-press)_var(--ease-out),translate_var(--sb-duration-press)_var(--ease-out)]\",\n  \"active:scale-(--sb-scale-press) active:translate-y-(--sb-offset-press)\",\n].join(\" \");\n\n/**\n * Recessed tray for segmented controls and tab lists: the well tone with an inset shadow, so the\n * raised pebble inside reads as floating on it.\n */\nexport const traySurface = \"rounded-lg bg-well shadow-tray\";\n\n/**\n * The pebble: the one raised element that marks the chosen segment or tab. It slides between\n * items on the snappy spring (CSS variables set from the measured item).\n */\nexport const pebble = [\n  \"pointer-events-none absolute left-0 top-0 z-0 rounded-md bg-raised shadow-pill\",\n  \"[transition:translate_var(--sb-duration-spring-snappy)_var(--sb-spring-snappy),width_var(--sb-duration-spring-snappy)_var(--sb-spring-snappy),height_var(--sb-duration-spring-snappy)_var(--sb-spring-snappy)]\",\n].join(\" \");\n\n/**\n * Text field and select trigger: a well with the control edge drawn as an inset\n * ring (3:1), not a border. Invalid swaps the ring to danger.\n */\nexport const fieldSurface = [\n  \"rounded-md bg-well text-fg shadow-field\",\n  \"transition-[box-shadow,background-color] duration-(--sb-duration-hover) ease-hover\",\n  \"hover:shadow-field-hover focus-visible:bg-surface\",\n  \"aria-invalid:bg-danger-soft aria-invalid:shadow-[inset_0_0_0_1.5px_var(--sb-danger-dot)]\",\n  \"data-invalid:bg-danger-soft data-invalid:shadow-[inset_0_0_0_1.5px_var(--sb-danger-dot)]\",\n  \"data-disabled:cursor-not-allowed data-disabled:opacity-55\",\n].join(\" \");\n\n/** Touch targets stay at 44px on coarse pointers whatever the density. */\nexport const coarseTarget = \"pointer-coarse:min-h-11\";\n\n/** Props of a Base UI part with `className` narrowed to a string (no state callback). */\nexport type WithClass<T> = Omit<T, \"className\"> & { className?: string };\n",
      "type": "registry:lib",
      "target": "@components/ikada/lib/cn.ts"
    }
  ],
  "categories": [
    "utilities"
  ],
  "type": "registry:lib"
}