{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "smooth-funnel",
  "title": "Smooth Funnel",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/smooth-funnel/SmoothFunnel.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface FunnelStage {\n    name: string\n    value: number\n    /** Optional override; defaults to chart-1..N from the registry palette. */\n    color?: string\n  }\n\n  export interface SmoothFunnelProps extends HTMLAttributes<HTMLDivElement> {\n    data: FunnelStage[]\n    /** Container height (px when numeric, raw CSS when string). Defaults to 240. */\n    height?: number | string\n    /** Show the percent pill on each stage. Defaults to true. */\n    showLabels?: boolean\n    /** Minimum segment height in px so tail stages stay visible at tiny percents. Default 18. */\n    minHeight?: number\n    /** Optional fallback palette when `color` is omitted on a stage. */\n    colors?: string[]\n    /** Accessible name announced for the chart image. Defaults to \"Chart\". */\n    ariaLabel?: string\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    data,\n    height = 240,\n    showLabels = true,\n    minHeight = 18,\n    colors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'],\n    ariaLabel,\n    ref = $bindable(null),\n    ...restProps\n  }: SmoothFunnelProps = $props()\n\n  // SVG geometry. Width/height are virtual (the SVG fits to container\n  // via viewBox). The aspect ratio (W:H ≈ 4:1) matches the horizontal\n  // \"flow\" layout consumers typically want for a 4-6 stage funnel; if\n  // you need taller bands, override `height` and the curves stretch\n  // vertically without distorting horizontally.\n  const W = 720\n  const H = 180\n  const cy = H / 2\n\n  const segments = $derived.by(() => {\n    const stages = data\n    const n = stages.length\n    if (n === 0) return []\n    const segW = W / n\n    const max = Math.max(...stages.map((s) => s.value))\n    const pctOf = (v: number) => (max > 0 ? (v / max) * 100 : 0)\n    const heightFor = (pct: number) => Math.max((pct / 100) * H, minHeight)\n\n    return stages.map((s, i) => {\n      const next = stages[i + 1] ?? s\n      const startPct = pctOf(s.value)\n      const endPct = pctOf(next.value)\n      const h0 = heightFor(startPct)\n      const h1 = heightFor(endPct)\n      const x0 = i * segW\n      const x1 = x0 + segW\n      const yTop0 = cy - h0 / 2\n      const yTop1 = cy - h1 / 2\n      const yBot0 = cy + h0 / 2\n      const yBot1 = cy + h1 / 2\n\n      // Cubic bezier control points at 38% / 62% of segment width produce\n      // a soft S-curve transition between stages rather than the\n      // trapezoidal default of an ECharts funnel.\n      const cx1 = x0 + segW * 0.38\n      const cx2 = x0 + segW * 0.62\n\n      const d = [\n        `M ${x0.toFixed(1)} ${yTop0.toFixed(1)}`,\n        `C ${cx1.toFixed(1)} ${yTop0.toFixed(1)}, ${cx2.toFixed(1)} ${yTop1.toFixed(1)}, ${x1.toFixed(1)} ${yTop1.toFixed(1)}`,\n        `L ${x1.toFixed(1)} ${yBot1.toFixed(1)}`,\n        `C ${cx2.toFixed(1)} ${yBot1.toFixed(1)}, ${cx1.toFixed(1)} ${yBot0.toFixed(1)}, ${x0.toFixed(1)} ${yBot0.toFixed(1)}`,\n        'Z',\n      ].join(' ')\n\n      return {\n        d,\n        color: s.color ?? colors[i % colors.length],\n        percent: startPct,\n        name: s.name,\n        value: s.value,\n        labelX: x0 + segW * 0.42,\n        labelY: cy,\n      }\n    })\n  })\n\n  const heightStyle = $derived(/^\\d+$/.test(String(height)) ? `${height}px` : String(height))\n</script>\n\n<!-- svelte-ignore a11y_no_noninteractive_tabindex -- focusable chart mirrors the Vue twin (keyboard focus ring on the chart). -->\n<div\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"smooth-funnel\"\n  tabindex=\"0\"\n  style=\"height:{heightStyle}\"\n  class={cn('focus-visible:ring-ring w-full focus-visible:ring-2 focus-visible:outline-none', className)}\n  {...restProps}\n>\n  <svg\n    viewBox=\"0 0 {W} {H}\"\n    class=\"block h-full w-full\"\n    preserveAspectRatio=\"none\"\n    role=\"img\"\n    aria-label={ariaLabel || 'Chart'}\n  >\n    {#each segments as seg, i (i)}\n      <g>\n        <path d={seg.d} fill={seg.color} />\n        {#if showLabels}\n          <foreignObject x={seg.labelX - 28} y={seg.labelY - 12} width=\"56\" height=\"24\">\n            <div\n              class=\"bg-background text-foreground inline-flex h-6 items-center rounded-full border px-2 text-xs font-semibold shadow-sm\"\n            >\n              {Math.round(seg.percent * 10) / 10}%\n            </div>\n          </foreignObject>\n        {/if}\n      </g>\n    {/each}\n  </svg>\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/smooth-funnel/SmoothFunnel.svelte"
    },
    {
      "path": "packages/registry-svelte/components/smooth-funnel/index.ts",
      "content": "export { default as SmoothFunnel, type FunnelStage, type SmoothFunnelProps } from './SmoothFunnel.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/smooth-funnel/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Smoothly tapering SVG funnel with cubic-bezier transitions between stages. Pure SVG (no ECharts). Each stage is colored independently and shows its own percent pill; a minHeight floor keeps tail stages visible at tiny percents.",
  "categories": [
    "chart"
  ]
}