{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-property-core",
  "title": "Property core primitive",
  "description": "Shared pieces for Eased real-estate blocks: CRE units (SF, $/SF, terms, cap rates), lease-expiry status and its tints, the vacancy hatch and an expiry legend.",
  "dependencies": [],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/property-core/property-core.tsx",
      "content": "/**\n * What every Eased real-estate block shares, so a pipeline, a stacking plan, a lease timeline\n * and a comps table read as one product: CRE units (SF, $/SF/yr, terms), the lease-expiry\n * status and its colours, and the vacancy hatch.\n */\nimport { trueMinus } from \"../../lib/format\";\n\nexport const RE_LOCALE = \"en-US\";\n\nconst cache = new Map<string, Intl.NumberFormat>();\nconst nf = (locale: string, o: Intl.NumberFormatOptions) => {\n  const k = `${locale}|${JSON.stringify(o)}`;\n  let f = cache.get(k);\n  if (!f) cache.set(k, (f = new Intl.NumberFormat(locale, o)));\n  return f;\n};\n\n/** Format options for NumberRoll and Intl: whole dollars, or `$3.7M` with `compact`. */\nexport const usdOptions = (compact = false, currency = \"USD\"): Intl.NumberFormatOptions =>\n  compact\n    ? { style: \"currency\", currency, notation: \"compact\", maximumFractionDigits: 1 }\n    : { style: \"currency\", currency, maximumFractionDigits: 0 };\n/** Rent per square foot per year: `$52.50`. */\nexport const psfOptions = (currency = \"USD\"): Intl.NumberFormatOptions => ({ style: \"currency\", currency, minimumFractionDigits: 2, maximumFractionDigits: 2 });\n\n/** `$3,727,500`, or `$3.7M` with `compact`; negatives use the true minus. */\nexport const formatUsd = (v: number, { compact = false, locale = RE_LOCALE, currency = \"USD\" } = {}) => trueMinus(nf(locale, usdOptions(compact, currency)).format(v));\n/** `$52.50` (append the `/SF` unit yourself, muted). */\nexport const formatPsf = (v: number, { locale = RE_LOCALE, currency = \"USD\" } = {}) => trueMinus(nf(locale, psfOptions(currency)).format(v));\n/** `14,200 SF`; `compact` gives `14.2K SF`. */\nexport const formatSqft = (v: number, { compact = false, locale = RE_LOCALE } = {}) =>\n  `${nf(locale, compact ? { notation: \"compact\", maximumFractionDigits: 1 } : { maximumFractionDigits: 0 }).format(v)} SF`;\n/** `6.25%` from `0.0625`. */\nexport const formatRate = (v: number, { decimals = 2, locale = RE_LOCALE } = {}) =>\n  trueMinus(nf(locale, { style: \"percent\", minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(v));\n/** A lease term: `7 yr`, `5 yr 4 mo`, `9 mo`. */\nexport function formatTerm(months: number) {\n  const y = Math.floor(months / 12);\n  const m = Math.round(months % 12);\n  if (!y) return `${m} mo`;\n  return m ? `${y} yr ${m} mo` : `${y} yr`;\n}\n\nconst DAY = 86_400_000;\n/** Whole days from `now` to `t`, negative in the past. */\nexport const daysUntil = (t: number, now: number) => Math.round((t - now) / DAY);\n\n/** \"in 3 months\", \"in 2 years\", \"2 months ago\", through Intl.RelativeTimeFormat. */\nexport function relativeExpiry(t: number, now: number, locale = RE_LOCALE) {\n  const days = daysUntil(t, now);\n  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: \"auto\" });\n  if (Math.abs(days) < 45) return rtf.format(days, \"day\");\n  if (Math.abs(days) < 640) return rtf.format(Math.round(days / 30.44), \"month\");\n  return rtf.format(Math.round((days / 365.25) * 10) / 10, \"year\");\n}\n\n/**\n * A suite or lease as of a date: `vacant` (no tenant, or the lease has ended by then),\n * `expiring` (ends within `soonDays`, default a year: start the renewal talk) or `leased`.\n */\nexport type ExpiryStatus = \"leased\" | \"expiring\" | \"vacant\";\nexport function expiryStatus(expiresAt: number | null | undefined, asOf: number, { soonDays = 365 } = {}): ExpiryStatus {\n  if (expiresAt == null || expiresAt <= asOf) return \"vacant\";\n  return expiresAt - asOf <= soonDays * DAY ? \"expiring\" : \"leased\";\n}\n\n/** The vacancy hatch: 1px rules at 45° every 6px in the line colour, over the sunken surface. */\nexport const HATCH =\n  \"bg-surface-sunken bg-[repeating-linear-gradient(135deg,var(--eased-line)_0_1px,transparent_1px_6px)]\";\n\n/** Fills, borders and text for each status: tints mark state (R30, R31), vacancy is hatched. */\nexport const EXPIRY_TONE: Record<ExpiryStatus, { label: string; fill: string; text: string; swatch: string }> = {\n  leased: { label: \"Leased\", fill: \"bg-info-subtle\", text: \"text-info-text\", swatch: \"bg-info-subtle shadow-[inset_0_0_0_1px_var(--eased-info)]\" },\n  expiring: { label: \"Expires within 12 mo\", fill: \"bg-warning-subtle\", text: \"text-warning-text\", swatch: \"bg-warning-subtle shadow-[inset_0_0_0_1px_var(--eased-warning)]\" },\n  vacant: { label: \"Vacant\", fill: HATCH, text: \"text-text-muted\", swatch: `${HATCH} shadow-[inset_0_0_0_1px_var(--eased-line)]` },\n};\n\n/** A legend key for the statuses: a 10px swatch and its label, one row. */\nexport function ExpiryLegend({ statuses = [\"leased\", \"expiring\", \"vacant\"], labels, className = \"\" }: { statuses?: ExpiryStatus[]; labels?: Partial<Record<ExpiryStatus, string>>; className?: string }) {\n  return (\n    <ul className={`flex min-w-0 flex-wrap items-center gap-x-4 gap-y-1 text-ui-xs text-text-muted ${className}`}>\n      {statuses.map((s) => (\n        <li key={s} className=\"flex min-w-0 items-center gap-1.5\">\n          <span aria-hidden className={`size-2.5 shrink-0 rounded-control-2xs ${EXPIRY_TONE[s].swatch}`} />\n          <span className=\"truncate\">{labels?.[s] ?? EXPIRY_TONE[s].label}</span>\n        </li>\n      ))}\n    </ul>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/property-core/property-core.tsx"
    }
  ],
  "type": "registry:ui"
}