{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "description": "The `styles` prop engine: shorthands, token references, breakpoints and state selectors.",
  "docs": "Two steps after installing: import the copied stylesheet once at your app entry — it lands at `styles/liquefy-ui.css` — or `@import` it from your global CSS *before* `tailwindcss`, so Tailwind utilities keep winning over component styles. Then wrap the tree in `<LiquefyProvider>`, which is where every component reads its tokens from. Every copied file already carries its own 'use client' directive. Full documentation: https://liquefy-ui.com/#/docs/installation",
  "files": [
    {
      "content": "'use client'\n\nimport { useInsertionEffect, useMemo, type CSSProperties } from 'react'\nimport { useLiquefyConfig, type LiquefyBreakpoint, type LiquefyBreakpoints } from '@/lib/provider'\n\n// Declared locally so the package keeps working without @types/node, in both the\n// browser (where `process` is absent) and any bundler that inlines NODE_ENV.\ndeclare const process: { env?: Record<string, string | undefined> } | undefined\n\nconst isDevelopment = typeof process !== 'undefined' && process?.env?.NODE_ENV !== 'production'\n\n/* -------------------------------------------------------------------------- *\n * Public types\n * -------------------------------------------------------------------------- */\n\ntype Scalar = number | string\n\n/** A single value, or one value per breakpoint. `base` applies below the first breakpoint. */\nexport type LiquidResponsive<T> = T | ({ base?: T } & Partial<Record<LiquefyBreakpoint, T>>)\n\ntype StandardStyles = {\n  [K in keyof CSSProperties]?: LiquidResponsive<CSSProperties[K]>\n}\n\ntype CustomPropertyStyles = {\n  [K in `--${string}`]?: LiquidResponsive<Scalar>\n}\n\n/**\n * Shorthands. Spacing keys (`p*`, `m*`) read plain numbers as multiples of the\n * `--lq-space` scale; every other key reads numbers as pixels, matching `style`.\n */\ntype ShorthandStyles = {\n  bg?: LiquidResponsive<CSSProperties['backgroundColor']>\n  h?: LiquidResponsive<CSSProperties['height']>\n  m?: LiquidResponsive<Scalar>\n  maxH?: LiquidResponsive<CSSProperties['maxHeight']>\n  maxW?: LiquidResponsive<CSSProperties['maxWidth']>\n  mb?: LiquidResponsive<Scalar>\n  minH?: LiquidResponsive<CSSProperties['minHeight']>\n  minW?: LiquidResponsive<CSSProperties['minWidth']>\n  ml?: LiquidResponsive<Scalar>\n  mr?: LiquidResponsive<Scalar>\n  mt?: LiquidResponsive<Scalar>\n  mx?: LiquidResponsive<Scalar>\n  my?: LiquidResponsive<Scalar>\n  p?: LiquidResponsive<Scalar>\n  pb?: LiquidResponsive<Scalar>\n  pl?: LiquidResponsive<Scalar>\n  pr?: LiquidResponsive<Scalar>\n  pt?: LiquidResponsive<Scalar>\n  px?: LiquidResponsive<Scalar>\n  py?: LiquidResponsive<Scalar>\n  /** Drives `--lq-radius`, so the press-squish keeps animating the corners. */\n  radius?: LiquidResponsive<Scalar>\n  /** Sets width and height together. */\n  size?: LiquidResponsive<Scalar>\n  w?: LiquidResponsive<CSSProperties['width']>\n}\n\n/** State keys understood by `styles`. Anything else must be written as `&…` or `@…`. */\nexport type LiquidStyleState =\n  | '_active'\n  | '_checked'\n  | '_dark'\n  | '_disabled'\n  | '_even'\n  | '_expanded'\n  | '_first'\n  | '_focus'\n  | '_focusVisible'\n  | '_hover'\n  | '_invalid'\n  | '_last'\n  | '_light'\n  | '_odd'\n  | '_open'\n  | '_placeholder'\n  | '_readOnly'\n  | '_selected'\n\ntype ConditionalStyles = { [K in LiquidStyleState]?: LiquidStyles } & {\n  [K in `&${string}`]?: LiquidStyles\n} & {\n  [K in `@${string}`]?: LiquidStyles\n}\n\n/**\n * Style overrides for a Liquid component root.\n *\n * - every CSS property (camelCase) plus `--custom-properties`\n * - shorthands: `p`, `px`, `mt`, `w`, `h`, `size`, `bg`, `radius`, …\n * - `$token` anywhere in a string resolves to `var(--lq-token)`\n * - responsive objects: `{ base: 1, md: 3 }`\n * - states: `_hover`, `_focusVisible`, `_dark`, … and raw `&…` / `@…` keys\n */\nexport type LiquidStyles = StandardStyles & CustomPropertyStyles & ShorthandStyles & ConditionalStyles\n\n/** Mixed into every component's props. */\nexport type LiquidStyleProps = {\n  /**\n   * Token-aware style overrides applied to the component root. Static values\n   * become inline styles; responsive and stateful values become a generated\n   * class that sits outside the `liquefy-ui` cascade layer, so it always wins\n   * over the stylesheet without needing `!important`.\n   */\n  styles?: LiquidStyles\n}\n\n/* -------------------------------------------------------------------------- *\n * Maps\n * -------------------------------------------------------------------------- */\n\nconst SHORTHANDS: Record<string, readonly string[]> = {\n  bg: ['background-color'],\n  h: ['height'],\n  m: ['margin'],\n  maxH: ['max-height'],\n  maxW: ['max-width'],\n  mb: ['margin-bottom'],\n  minH: ['min-height'],\n  minW: ['min-width'],\n  ml: ['margin-left'],\n  mr: ['margin-right'],\n  mt: ['margin-top'],\n  mx: ['margin-inline'],\n  my: ['margin-block'],\n  p: ['padding'],\n  pb: ['padding-bottom'],\n  pl: ['padding-left'],\n  pr: ['padding-right'],\n  pt: ['padding-top'],\n  px: ['padding-inline'],\n  py: ['padding-block'],\n  size: ['width', 'height'],\n  w: ['width'],\n}\n\n// Numbers on these read as multiples of --lq-space rather than pixels. Unitless\n// numbers are invalid CSS for all of them, so there is nothing to be ambiguous about.\nconst SPACING_PROPS = new Set([\n  'column-gap',\n  'gap',\n  'margin',\n  'margin-block',\n  'margin-block-end',\n  'margin-block-start',\n  'margin-bottom',\n  'margin-inline',\n  'margin-inline-end',\n  'margin-inline-start',\n  'margin-left',\n  'margin-right',\n  'margin-top',\n  'padding',\n  'padding-block',\n  'padding-block-end',\n  'padding-block-start',\n  'padding-bottom',\n  'padding-inline',\n  'padding-inline-end',\n  'padding-inline-start',\n  'padding-left',\n  'padding-right',\n  'padding-top',\n  'row-gap',\n])\n\n// Everything else gets `px` appended, exactly like the `style` attribute does.\nconst UNITLESS_PROPS = new Set([\n  'animation-iteration-count',\n  'aspect-ratio',\n  'border-image-slice',\n  'box-flex',\n  'column-count',\n  'columns',\n  'flex',\n  'flex-grow',\n  'flex-shrink',\n  'fill-opacity',\n  'font-weight',\n  'grid-area',\n  'grid-column',\n  'grid-column-end',\n  'grid-column-start',\n  'grid-row',\n  'grid-row-end',\n  'grid-row-start',\n  'line-clamp',\n  'line-height',\n  'opacity',\n  'order',\n  'orphans',\n  'scale',\n  'stroke-opacity',\n  'stroke-width',\n  'tab-size',\n  'widows',\n  'z-index',\n  'zoom',\n])\n\n// Bare color words on colour-ish properties, so `color: 'accent'` reads well.\nconst COLOR_WORDS = new Set([\n  'accent',\n  'foreground',\n  'line',\n  'muted',\n  'placeholder',\n  'text',\n  'tint',\n])\n\nconst isColorProp = (prop: string): boolean =>\n  prop === 'color' || prop === 'fill' || prop === 'stroke' || prop.endsWith('color')\n\ntype Variant = { at?: string; selector: string }\n\nconst STATES: Record<string, readonly Variant[]> = {\n  _active: [{ selector: '&:active' }],\n  _checked: [{ selector: \"&:checked, &[aria-checked='true'], &[data-checked='true']\" }],\n  // theme='system' resolves through the media query instead of the attribute.\n  _dark: [\n    { selector: \"[data-liquid-theme='dark'] &\" },\n    { at: '@media (prefers-color-scheme: dark)', selector: \"[data-liquid-theme='system'] &\" },\n  ],\n  _disabled: [{ selector: \"&:disabled, &[aria-disabled='true'], &[data-disabled='true']\" }],\n  _even: [{ selector: '&:nth-child(even)' }],\n  _expanded: [{ selector: \"&[aria-expanded='true']\" }],\n  _first: [{ selector: '&:first-child' }],\n  _focus: [{ selector: '&:focus' }],\n  _focusVisible: [{ selector: '&:focus-visible' }],\n  _hover: [{ selector: '&:hover' }],\n  _invalid: [{ selector: \"&:invalid, &[aria-invalid='true'], &[data-invalid='true']\" }],\n  _last: [{ selector: '&:last-child' }],\n  _light: [\n    { selector: \"[data-liquid-theme='light'] &\" },\n    { at: '@media (prefers-color-scheme: light)', selector: \"[data-liquid-theme='system'] &\" },\n  ],\n  _odd: [{ selector: '&:nth-child(odd)' }],\n  _open: [{ selector: \"&[open], &[data-open='true']\" }],\n  _placeholder: [{ selector: '&::placeholder' }],\n  _readOnly: [{ selector: \"&:read-only, &[aria-readonly='true']\" }],\n  _selected: [\n    { selector: \"&[aria-selected='true'], &[data-selected='true'], &[aria-current='page']\" },\n  ],\n}\n\n// The physics engine writes these inline every frame; a style override loses the\n// race silently, so it is worth saying so out loud in development.\nconst PHYSICS_OWNED = new Set(['backdrop-filter', 'transform'])\n\n/* -------------------------------------------------------------------------- *\n * Value + property resolution\n * -------------------------------------------------------------------------- */\n\nconst hyphenate = (key: string): string =>\n  key.startsWith('--') ? key : key.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)\n\nconst camelize = (prop: string): string =>\n  prop.startsWith('--') ? prop : prop.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase())\n\n/** `$accent` → `var(--lq-accent)`, anywhere inside a string. */\nconst resolveTokens = (value: string): string => value.replace(/\\$([a-zA-Z][\\w-]*)/g, 'var(--lq-$1)')\n\nconst resolveValue = (prop: string, value: Scalar): string => {\n  if (typeof value === 'number') {\n    if (SPACING_PROPS.has(prop)) return value === 0 ? '0' : `calc(var(--lq-space, 4px) * ${value})`\n    if (UNITLESS_PROPS.has(prop) || prop.startsWith('--')) return String(value)\n    return value === 0 ? '0' : `${value}px`\n  }\n  if (isColorProp(prop) && COLOR_WORDS.has(value)) return `var(--lq-${value})`\n  return resolveTokens(value)\n}\n\ntype Declaration = readonly [prop: string, value: string]\n\nconst declare = (key: string, value: Scalar): Declaration[] => {\n  if (key === 'radius') {\n    const resolved = typeof value === 'number' ? `${value}px` : resolveTokens(value)\n    // Keeping the squish term means custom corners still breathe under a press.\n    return [\n      ['--lq-radius', resolved],\n      ['border-radius', 'calc(var(--lq-radius) + var(--lq-squish, 0) * 6px)'],\n    ]\n  }\n  const props = SHORTHANDS[key] ?? [hyphenate(key)]\n  return props.map((prop) => [prop, resolveValue(prop, value)] as const)\n}\n\n/* -------------------------------------------------------------------------- *\n * Parsing\n * -------------------------------------------------------------------------- */\n\ntype Block = {\n  at: readonly string[]\n  decls: Declaration[]\n  order: number\n  selector: string\n}\n\ntype Parsed = {\n  /** Static top-level declarations, ready for the `style` attribute. */\n  inline: CSSProperties | undefined\n  /** Hyphenated property names declared at the top level, whatever route they took. */\n  owned: readonly string[]\n  rules: string | undefined\n  token: string | undefined\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst toLength = (value: number | string): string => (typeof value === 'number' ? `${value}px` : value)\n\nconst parse = (\n  styles: LiquidStyles,\n  breakpoints: LiquefyBreakpoints,\n  order: readonly LiquefyBreakpoint[],\n): { blocks: Block[]; owned: string[] } => {\n  const blocks: Block[] = []\n  const owned: string[] = []\n\n  const walk = (node: Record<string, unknown>, at: readonly string[], selector: string, top: boolean) => {\n    // Reserved up front so unconditional declarations always precede the\n    // breakpoint and state blocks they are meant to be overridden by.\n    const base: Block = { at, decls: [], order: 0, selector }\n    blocks.push(base)\n    const deferred: (() => void)[] = []\n\n    for (const [key, value] of Object.entries(node)) {\n      if (value === undefined || value === null || value === false) continue\n\n      const state = STATES[key]\n      if (state) {\n        for (const variant of state) {\n          deferred.push(() =>\n            walk(\n              value as Record<string, unknown>,\n              variant.at ? [...at, variant.at] : at,\n              variant.selector.includes('&') ? variant.selector.replace(/&/g, selector) : variant.selector,\n              false,\n            ),\n          )\n        }\n        continue\n      }\n\n      if (key.startsWith('&')) {\n        deferred.push(() => walk(value as Record<string, unknown>, at, key.replace(/&/g, selector), false))\n        continue\n      }\n\n      if (key.startsWith('@')) {\n        deferred.push(() => walk(value as Record<string, unknown>, [...at, key], selector, false))\n        continue\n      }\n\n      if (isPlainObject(value)) {\n        const { base: baseValue } = value as { base?: Scalar }\n        if (baseValue !== undefined && baseValue !== null) {\n          base.decls.push(...declare(key, baseValue))\n        }\n        for (const [index, name] of order.entries()) {\n          const atBreakpoint = value[name] as Scalar | undefined\n          if (atBreakpoint === undefined || atBreakpoint === null) continue\n          const query = `@media (min-width: ${toLength(breakpoints[name])})`\n          const decls = declare(key, atBreakpoint)\n          deferred.push(() => {\n            blocks.push({ at: [...at, query], decls, order: index + 1, selector })\n          })\n        }\n        // Recorded even when only a breakpoint sets it: the class has to be the\n        // only route to the property, or the inline fallback would outrank it.\n        if (top) owned.push(...declare(key, 0).map(([prop]) => prop))\n        continue\n      }\n\n      const decls = declare(key, value as Scalar)\n      base.decls.push(...decls)\n      if (top) owned.push(...decls.map(([prop]) => prop))\n    }\n\n    for (const run of deferred) run()\n  }\n\n  walk(styles as Record<string, unknown>, [], '&', true)\n  return { blocks: blocks.filter((block) => block.decls.length > 0), owned }\n}\n\n/* -------------------------------------------------------------------------- *\n * Sheet\n * -------------------------------------------------------------------------- */\n\nconst hash = (input: string): string => {\n  let value = 5381\n  for (let index = 0; index < input.length; index += 1) {\n    value = (value * 33) ^ input.charCodeAt(index)\n  }\n  return (value >>> 0).toString(36)\n}\n\nconst collected = new Map<string, string>()\nlet sheet: HTMLStyleElement | null = null\n\nconst insert = (token: string, rules: string): void => {\n  if (collected.has(token)) return\n  collected.set(token, rules)\n  if (typeof document === 'undefined') return\n  if (!sheet) {\n    sheet = document.querySelector<HTMLStyleElement>('style[data-liquefy-styles]')\n    if (!sheet) {\n      sheet = document.createElement('style')\n      sheet.setAttribute('data-liquefy-styles', '')\n      document.head.append(sheet)\n    }\n  }\n  sheet.append(document.createTextNode(rules))\n}\n\n/**\n * Every rule generated by `styles` so far, as CSS text. Server renderers can\n * inline this into the document head; the generated rules are intentionally\n * unlayered so they win over `@layer liquefy-ui` without `!important`.\n */\nexport const getLiquefyStyleSheet = (): string => [...collected.values()].join('')\n\n/* -------------------------------------------------------------------------- *\n * Hook\n * -------------------------------------------------------------------------- */\n\nconst serializeBlock = (block: Block, selector: string): string => {\n  const body = block.decls.map(([prop, value]) => `${prop}:${value}`).join(';')\n  let rule = `${block.selector.replace(/&/g, selector)}{${body}}`\n  for (const at of [...block.at].reverse()) {\n    rule = `${at}{${rule}}`\n  }\n  return rule\n}\n\n/**\n * Pure compiler behind {@link useLiquidStyles}. Exported for the unit tests; it\n * is deliberately absent from the package entry point.\n */\nexport const compileLiquidStyles = (\n  styles: LiquidStyles | undefined,\n  breakpoints: LiquefyBreakpoints,\n  order: readonly LiquefyBreakpoint[],\n): Parsed => {\n  if (!styles) return { inline: undefined, owned: [], rules: undefined, token: undefined }\n\n  const { blocks, owned } = parse(styles, breakpoints, order)\n  if (blocks.length === 0) return { inline: undefined, owned, rules: undefined, token: undefined }\n\n  if (isDevelopment) {\n    for (const prop of owned) {\n      if (PHYSICS_OWNED.has(prop)) {\n        console.warn(\n          `[liquefy-ui] styles.${camelize(prop)} is overwritten every frame by the jelly springs. ` +\n            'Wrap the component in your own element and style that instead.',\n        )\n      }\n    }\n  }\n\n  // A single unconditional block can ride on the style attribute — no class, no\n  // stylesheet, no hydration concerns. The moment a state or breakpoint appears\n  // everything has to move into the sheet together, or the inline declarations\n  // would outrank the very rules meant to override them.\n  const [only] = blocks\n  if (blocks.length === 1 && only && only.at.length === 0 && only.selector === '&') {\n    const inline: Record<string, string> = {}\n    for (const [prop, value] of only.decls) inline[camelize(prop)] = value\n    return { inline: inline as CSSProperties, owned, rules: undefined, token: undefined }\n  }\n\n  const sorted = [...blocks].sort((left, right) => left.order - right.order)\n  const token = `lq-x-${hash(JSON.stringify(sorted))}`\n  const rules = sorted.map((block) => serializeBlock(block, `.${token}`)).join('')\n  return { inline: undefined, owned, rules, token }\n}\n\n/** A `style` object that also accepts custom properties. */\nexport type LiquidCustomProperties = CSSProperties & Record<`--${string}`, number | string>\n\ntype StyledInput = {\n  className?: string\n  style?: CSSProperties\n  styles?: LiquidStyles\n  /** Component-owned custom properties. `styles` and `style` both outrank them. */\n  vars?: LiquidCustomProperties\n}\n\nexport type StyledRoot = {\n  className: string\n  style: CSSProperties | undefined\n}\n\n/**\n * Resolves the `styles` prop into a `className` / `style` pair for a component\n * root, merging the component's own classes and custom properties.\n */\nexport const useLiquidStyles = (\n  base: string | readonly (string | false | undefined)[],\n  { className, style, styles, vars }: StyledInput,\n): StyledRoot => {\n  const { breakpoints } = useLiquefyConfig()\n  const key = styles ? JSON.stringify(styles) : ''\n  const order = useMemo(\n    () =>\n      (Object.keys(breakpoints) as LiquefyBreakpoint[]).sort(\n        (left, right) => parseFloat(toLength(breakpoints[left])) - parseFloat(toLength(breakpoints[right])),\n      ),\n    [breakpoints],\n  )\n  const compiled = useMemo(\n    () => compileLiquidStyles(styles, breakpoints, order),\n    // The serialized form is the real identity here; inline object literals\n    // would otherwise recompile on every render.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [key, breakpoints, order],\n  )\n\n  useInsertionEffect(() => {\n    if (compiled.token && compiled.rules) insert(compiled.token, compiled.rules)\n  }, [compiled.token, compiled.rules])\n\n  if (typeof document === 'undefined' && compiled.token && compiled.rules) {\n    insert(compiled.token, compiled.rules)\n  }\n\n  const merged = useMemo(() => {\n    if (!vars && !compiled.inline && !style) return undefined\n    const next: Record<string, unknown> = {}\n    if (vars) {\n      for (const [prop, value] of Object.entries(vars)) {\n        // Anything `styles` declares must reach the element through `styles`,\n        // otherwise a component-owned inline var would outrank the class.\n        if (compiled.owned.includes(hyphenate(prop))) continue\n        next[prop] = value\n      }\n    }\n    Object.assign(next, compiled.inline, style)\n    return next as CSSProperties\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [compiled, JSON.stringify(vars ?? null), style])\n\n  const classes = typeof base === 'string' ? [base] : [...base]\n  return {\n    className: [...classes, compiled.token, className].filter(Boolean).join(' '),\n    style: merged,\n  }\n}\n",
      "path": "registry/styles-prop.ts",
      "type": "registry:lib"
    }
  ],
  "name": "styles-prop",
  "registryDependencies": [
    "https://liquefy-ui.com/r/liquefy-ui.json",
    "https://liquefy-ui.com/r/provider.json"
  ],
  "title": "getLiquefyStyleSheet",
  "type": "registry:lib"
}
