{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bar-depth",
  "type": "registry:component",
  "title": "Bar Depth",
  "description": "3D depth and glossy glass surfaces for BarChart bars",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "@bklit/bar-chart"
  ],
  "files": [
    {
      "path": "src/charts/bar-depth.tsx",
      "content": "\"use client\";\n\n/**\n * Bar 3D depth + glass surfaces — drop-in layers for `<BarChart>`.\n *\n * Adds a head-on perspective and a glossy glass-block sheen to the bars in\n * any `<BarChart>`. Bars left of chart center expose their RIGHT side face;\n * bars right of center expose their LEFT — the vanishing point sits at the\n * chart's horizontal middle, so the row reads like vertical glass blocks\n * viewed straight on.\n *\n * Three surfaces per bar:\n *   1. Side face — a parallelogram on the depth side, lit at the front edge\n *      with a horizontal black gradient and shaded toward the back.\n *   2. Top face — a parallelogram lid above the bar's top edge, with a\n *      bright catch at the front lip fading to soft white at the back.\n *   3. Glass overlay — a vertical white-to-dark gradient on the bar's front\n *      face (sharp top reflection, transparent middle, faint bottom shadow).\n *\n * Usage (purely additive — no changes to `<Bar>`):\n *\n *   <BarChart data={data} xDataKey=\"month\">\n *     <Grid horizontal />\n *     <BarDepthBack dataKey=\"value\" color=\"var(--chart-1)\" />  // before <Bar>\n *     <Bar dataKey=\"value\" fill=\"var(--chart-1)\" />\n *     <BarDepthFront dataKey=\"value\" />                        // after <Bar>\n *   </BarChart>\n *\n * Both depth layers read geometry from `useChart()` and figure out the\n * perspective themselves. The only configuration is the bar color\n * (`color` / `colorAccessor`) and, for stacked bars, a `segmentsAccessor`\n * supplied via `<BarDepthProvider>`.\n *\n * An optional `<BarPulse>` paints a looping vertical sweep over a single\n * \"active\" bar (selected by `activeIndex`) — useful for highlighting a live\n * or in-progress value. Render it AFTER `<BarDepthFront>`.\n *\n * Behavior notes:\n *   - Negative values are supported: a bar whose value is below zero grows\n *     downward from the baseline, with the lid sitting on the baseline.\n *     (This requires the host `<BarChart>` to use a value scale whose\n *     domain includes negatives.)\n *   - Zero-value bars are skipped by default; set `minBarHeight` on\n *     `<BarDepthProvider>` (and `<Bar>`) to floor them to a short visible bar.\n *\n * Performance:\n *   - Three shared SVG gradients per layer instance (ID-collision-safe via\n *     `useId()`).\n *   - A per-bar `<clipPath>` for the side face animates from the baseline so\n *     the static side path is revealed bottom-up in lockstep with the bar.\n *     Only used where depth ≥ 0.5px.\n *   - At high bar density the column gap clamps depth below the 0.5px\n *     threshold; the back surfaces skip rendering and only the cheap glass\n *     rect remains.\n *   - `useBarDepthEntries` is memoized, so the Back/Front/Pulse layers\n *     compute geometry at most once per render between them.\n */\n\nimport { motion, type Transition } from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  useContext,\n  useId,\n  useMemo,\n} from \"react\";\nimport { barDepthAndRise, barDepthMaxDepth } from \"./bar-depth-geometry\";\nimport { useChartHover, useChartStable } from \"./chart-context\";\nimport { transitionWithDelay } from \"./motion-utils\";\n\n// ─── Constants ───────────────────────────────────────────────────────\n\n/** Below this depth the back surfaces don't render — saves elements for\n * dead-center bars and dense charts where perspective is sub-pixel. */\nconst BAR_DEPTH_MIN_PX = 0.5;\n/** Mirrors `Bar`'s default `fadedOpacity` so depth/glass dim in step with\n * their bar when a sibling bar is hovered. */\nconst BAR_FADED_OPACITY = 0.3;\n/** Default bar color when neither `color` nor `colorAccessor` is provided. */\nconst DEFAULT_BAR_COLOR = \"var(--chart-line-primary)\";\n/** Default opacity of the baseline \"contact shadow\" that grounds each bar at\n * the zero line. Configurable via `<BarDepthProvider groundShadow>`. */\nconst DEFAULT_GROUND_SHADOW = 0.26;\n/** Peak opacity of the white reflection catch at each bar's tip. Kept here so\n * the front/side glass and the solid lid highlight all use the same value. */\nconst GLASS_TIP_OPACITY = 0.2;\n\n// `BarPulse` — vertical sweep wave constants. The wave is a tall translucent\n// rect with a centered bright band; it translates from below the bar's bottom\n// edge to above the lid's back edge on a loop, clipped to the bar's full 3D\n// silhouette.\n/** Wave height = max(barHeight × this, PULSE_WAVE_HEIGHT_MIN_PX). 0.55 puts\n * the bright band's peak in the middle of the bar at mid-travel. */\nconst PULSE_WAVE_HEIGHT_RATIO = 0.55;\n/** Minimum wave height in px so short bars still pulse on a steady cadence —\n * the wave is taller than the bar so the bright band sweeps through cleanly\n * instead of flashing for one frame. */\nconst PULSE_WAVE_HEIGHT_MIN_PX = 36;\n/** Seconds per sweep (bottom → above the lid). 2.4s reads as a deliberate\n * heartbeat; faster feels frantic, slower feels laggy. */\nconst PULSE_WAVE_DURATION_S = 2.4;\n/** Peak opacity of the wave's brightest gradient stop. White at 0.85 is\n * bright enough to register over saturated bar colors while the bell-curve\n * stops around it keep the falloff soft so the bar's hue still dominates. */\nconst PULSE_WAVE_PEAK_OPACITY = 0.85;\n\nconst BAR_HOVER_TRANSITION = { duration: 0.15, ease: \"easeOut\" as const };\n\n// ─── Types ───────────────────────────────────────────────────────────\n\nexport interface BarDepthEntry {\n  label: string;\n  /** Index in the original `data` array — used for stagger calc. NOT the\n   * index in this entries list. */\n  dataIndex: number;\n  /** Original datum reference — handed back to `colorAccessor` /\n   * `segmentsAccessor` so consumers can color or split bars without\n   * bar-depth knowing the data shape. */\n  datum: Record<string, unknown>;\n  /** True when this bar is the active bar (`dataIndex === activeIndex`).\n   * Used only by `<BarPulse>`; geometry is identical regardless. */\n  isActive: boolean;\n  /** True when the bar's value is negative — the bar extends downward from\n   * the baseline. The lid still sits at the baseline; the side parallelogram\n   * grows downward instead of upward. */\n  isNegative: boolean;\n  /** y of the chart's baseline (= `yScale(0)`). For positive bars this equals\n   * `bottomY`; for negative bars it equals `topY`. Used as the grow-from\n   * anchor for both the clipPath (side face) and the glass rect. */\n  baselineY: number;\n  bandX: number;\n  bandWidth: number;\n  /** Visual top of the bar in screen Y (smaller y = higher on screen). */\n  topY: number;\n  /** Visual bottom of the bar in screen Y. */\n  bottomY: number;\n  barHeight: number;\n  /** Un-trimmed natural pixel height of the bar (= `|baselineY - yScale(value)|`).\n   * Segment heights are scaled against this so they line up with `<Bar>`. */\n  naturalHeight: number;\n  /** Pixels subtracted from the bar's visual top (and added to `topY`) so the\n   * lid's back edge lands at `yScale(value)` regardless of depth. */\n  topYTrim: number;\n  /** Side-face thickness in px (0 = dead-center bar / dense chart). */\n  depth: number;\n  perspectiveRise: number;\n  isRightOfCenter: boolean;\n}\n\n/** One segment of a stacked bar, in stack order bottom→top. The values across\n * all segments should sum to the bar's value; bar-depth uses the proportions\n * to pick segment boundaries that line up with the front face's stacking. */\nexport interface BarDepthSegment {\n  value: number;\n  color: string;\n}\n\n/** Resolve a per-datum color, falling back to the shared `color` default. */\ntype ColorAccessor = (datum: Record<string, unknown>, index: number) => string;\n\nfunction resolveColor(\n  datum: Record<string, unknown>,\n  index: number,\n  color: string,\n  colorAccessor?: ColorAccessor\n): string {\n  return colorAccessor?.(datum, index) ?? color;\n}\n\n/** Side-face parallelogram between two screen-Y edges, on the depth side of\n * the bar (left of the bar for right-of-center bars, right for left-of). */\nfunction sideFacePath(\n  e: BarDepthEntry,\n  topEdge: number,\n  bottomEdge: number\n): string {\n  const rise = e.perspectiveRise;\n  if (e.isRightOfCenter) {\n    const x = e.bandX;\n    return `M ${x} ${topEdge} L ${x - e.depth} ${topEdge - rise} L ${x - e.depth} ${bottomEdge - rise} L ${x} ${bottomEdge} Z`;\n  }\n  const x = e.bandX + e.bandWidth;\n  return `M ${x} ${topEdge} L ${x + e.depth} ${topEdge - rise} L ${x + e.depth} ${bottomEdge - rise} L ${x} ${bottomEdge} Z`;\n}\n\n/** Lid (top face) parallelogram above the bar's top edge. */\nfunction lidFacePath(e: BarDepthEntry): string {\n  const rise = e.perspectiveRise;\n  const left = e.bandX;\n  const right = e.bandX + e.bandWidth;\n  if (e.isRightOfCenter) {\n    return `M ${left} ${e.topY} L ${right} ${e.topY} L ${right - e.depth} ${e.topY - rise} L ${left - e.depth} ${e.topY - rise} Z`;\n  }\n  return `M ${left} ${e.topY} L ${right} ${e.topY} L ${right + e.depth} ${e.topY - rise} L ${left + e.depth} ${e.topY - rise} Z`;\n}\n\n/**\n * Build the side-face parallelograms for a bar. Without segments it's a single\n * piece in `baseColor`; with segments each gets its natural scaled height\n * `(seg.value/total) * naturalHeight` and only the topmost has `topYTrim`\n * removed — matching exactly how `<Bar>` stacks segments, so the side seams\n * line up with the front face.\n */\nfunction buildSidePieces(\n  e: BarDepthEntry,\n  visibleSegments: BarDepthSegment[] | null,\n  baseColor: string\n): { path: string; color: string }[] {\n  if (!(visibleSegments && visibleSegments.length > 0)) {\n    return [{ path: sideFacePath(e, e.topY, e.bottomY), color: baseColor }];\n  }\n  const totalValue = visibleSegments.reduce((sum, s) => sum + s.value, 0);\n  const pieces: { path: string; color: string }[] = [];\n  let cursorY = e.bottomY;\n  for (let i = 0; i < visibleSegments.length; i++) {\n    const seg = visibleSegments[i];\n    if (!seg) {\n      continue;\n    }\n    const isTopmost = i === visibleSegments.length - 1;\n    const segScaledHeight = (seg.value / totalValue) * e.naturalHeight;\n    const segHeight = isTopmost\n      ? Math.max(0, segScaledHeight - e.topYTrim)\n      : segScaledHeight;\n    const segBottomY = cursorY;\n    const segTopY = cursorY - segHeight;\n    cursorY = segTopY;\n    pieces.push({\n      path: sideFacePath(e, segTopY, segBottomY),\n      color: seg.color,\n    });\n  }\n  return pieces;\n}\n\n// ─── Context ──────────────────────────────────────────────────────────\n// `BarDepthProvider` is the single source of truth for `segmentsAccessor`,\n// shared by every depth surface (`<BarDepthBack>`, `<BarDepthFront>`,\n// `<BarPulse>`) AND by the entries hook those surfaces consume.\n//\n// Why context instead of per-component props: `useBarDepthEntries` computes\n// `topYTrim` from the topmost segment's scaled height, so all three layers\n// need the *same* `segmentsAccessor` to produce the same `topY`. A single\n// provider above the chart makes it structurally impossible for them to\n// disagree.\n//\n// `BarChart` reads its `<Bar>` children with `React.Children.forEach`, which\n// doesn't recurse into a provider. Wrap the provider *around* the\n// `<BarChart>`, not inside it.\n\ninterface BarDepthContextValue {\n  segmentsAccessor?: (\n    datum: Record<string, unknown>\n  ) => BarDepthSegment[] | null | undefined;\n  /** Opacity (0–1) of the baseline contact shadow. 0 removes it. */\n  groundShadow?: number;\n  /** Minimum bar height in px — floors short/zero bars so they stay visible.\n   * Match the value passed to `<Bar minBarHeight>`. */\n  minBarHeight?: number;\n}\n\nconst BarDepthContext = createContext<BarDepthContextValue>({});\n\nexport interface BarDepthProviderProps extends BarDepthContextValue {\n  children: ReactNode;\n}\n\n/**\n * Provides shared bar-depth configuration to every depth surface beneath it.\n * Wrap a `<BarChart>` whose children include `<BarDepthBack>` /\n * `<BarDepthFront>` / `<BarPulse>` with this provider to:\n *   - split stacked bars into per-segment side faces (`segmentsAccessor`),\n *   - tune the baseline contact shadow (`groundShadow`, default 0.26; 0 = off),\n *   - floor short/zero bars so they stay visible (`minBarHeight`; pair with the\n *     same value on `<Bar minBarHeight>`).\n *\n * Because the glass gradient is chart-wide (one Y-anchored ramp shared by the\n * front face and the side glass), the provider is the single source of truth —\n * so the surfaces can never disagree.\n */\nexport function BarDepthProvider({\n  segmentsAccessor,\n  groundShadow,\n  minBarHeight,\n  children,\n}: BarDepthProviderProps) {\n  // Memoize so consumers don't re-render when the config is stable.\n  const value = useMemo<BarDepthContextValue>(\n    () => ({ segmentsAccessor, groundShadow, minBarHeight }),\n    [segmentsAccessor, groundShadow, minBarHeight]\n  );\n  return (\n    <BarDepthContext.Provider value={value}>\n      {children}\n    </BarDepthContext.Provider>\n  );\n}\n\nBarDepthProvider.displayName = \"BarDepthProvider\";\n\n// ─── Per-bar glass gradient ──────────────────────────────────────────\n// The glass is anchored to EACH BAR's own height (objectBoundingBox: 0% = the\n// bar's tip, 100% = its baseline), not to the chart. So every bar — tall or\n// short — gets the identical proportional gloss. (Anchoring per-chart made a\n// tall bar's tip bright while a short bar's tip fell in the transparent middle\n// and read as matte.)\n\ninterface GlassGradientStop {\n  offset: string;\n  color: string;\n  opacity: string;\n}\n\n/**\n * Stop list for a POSITIVE bar's glass (0% = tip, 100% = baseline): a sharp\n * white reflection catch at the tip falling off fast to transparent, then the\n * contact shadow at the base.\n */\nfunction buildPosBarStops(groundShadow: number): GlassGradientStop[] {\n  return [\n    { offset: \"0%\", color: \"white\", opacity: String(GLASS_TIP_OPACITY) },\n    { offset: \"3%\", color: \"white\", opacity: \"0.09\" },\n    { offset: \"8%\", color: \"white\", opacity: \"0.02\" },\n    { offset: \"55%\", color: \"white\", opacity: \"0\" },\n    { offset: \"100%\", color: \"black\", opacity: String(groundShadow) },\n  ];\n}\n\n/**\n * Mirror of `buildPosBarStops` for NEGATIVE bars (which grow downward): the\n * contact shadow sits at the top (the baseline) and the reflection catch at\n * the bottom (the bar's tip).\n */\nfunction buildNegBarStops(groundShadow: number): GlassGradientStop[] {\n  return [\n    { offset: \"0%\", color: \"black\", opacity: String(groundShadow) },\n    { offset: \"45%\", color: \"white\", opacity: \"0\" },\n    { offset: \"92%\", color: \"white\", opacity: \"0.02\" },\n    { offset: \"97%\", color: \"white\", opacity: \"0.09\" },\n    { offset: \"100%\", color: \"white\", opacity: String(GLASS_TIP_OPACITY) },\n  ];\n}\n\n/**\n * Build both per-bar glass stop lists from the current `groundShadow`. Each\n * depth surface picks `posStops` or `negStops` by the bar's direction and\n * paints it objectBoundingBox, so the gloss is identical across bar heights.\n */\nfunction useGlassStops() {\n  const { groundShadow = DEFAULT_GROUND_SHADOW } = useContext(BarDepthContext);\n  return useMemo(\n    () => ({\n      posStops: buildPosBarStops(groundShadow),\n      negStops: buildNegBarStops(groundShadow),\n    }),\n    [groundShadow]\n  );\n}\n\nfunction readBarValue(d: Record<string, unknown>, key: string): number {\n  const v = key ? d[key] : undefined;\n  return typeof v === \"number\" ? v : 0;\n}\n\n// ─── Hooks ───────────────────────────────────────────────────────────\n\n/**\n * Compute per-bar 3D depth geometry from chart context.\n *\n * Memoized on the chart's scales + data, so calling this from sibling layers\n * within the same chart is essentially free after the first settles. Returns\n * one entry per renderable bar; bars with no resolvable height are excluded.\n *\n * Negative-value bars are kept — `isNegative` flips the entry's geometry so\n * the side parallelogram and lid render in the correct direction.\n *\n * Exported for advanced consumers that want to render custom layers aligned\n * with the same geometry (e.g. labels above each bar's top face).\n */\nexport function useBarDepthEntries(\n  dataKey: string,\n  activeIndex?: number\n): BarDepthEntry[] {\n  const {\n    data,\n    barScale,\n    bandWidth,\n    yScale,\n    innerHeight,\n    innerWidth,\n    barXAccessor,\n  } = useChartStable();\n  // Read shared config from `<BarDepthProvider>`. Outside a provider these are\n  // the defaults (no segmentation, no floor) — correct for simple charts.\n  const { segmentsAccessor, minBarHeight = 0 } = useContext(BarDepthContext);\n\n  return useMemo(() => {\n    if (!(barScale && bandWidth && barXAccessor)) {\n      return [];\n    }\n\n    const centerX = innerWidth / 2;\n    const zeroY = yScale(0) ?? innerHeight;\n\n    // d3-scaleBand exposes step() (= bandwidth + gap); the shared helper caps\n    // depth below the gap so a side face never spills past the next bar.\n    const step =\n      (barScale as unknown as { step?: () => number }).step?.() ?? bandWidth;\n    const maxDepth = barDepthMaxDepth(step, bandWidth);\n\n    // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: per-bar 3D geometry (direction, perspective trim, segment scaling) reads clearest as one pass\n    return data.flatMap((d, dataIndex): BarDepthEntry[] => {\n      // Stacked bars: the bar's value is the SUM of its segments (so the depth\n      // height matches the host BarChart's stacked total). Single bars: the\n      // `dataKey` column. `segmentsAccessor` comes from `<BarDepthProvider>`.\n      const visibleSegs =\n        segmentsAccessor?.(d)?.filter((s) => s.value > 0) ?? null;\n      const value =\n        visibleSegs && visibleSegs.length > 0\n          ? visibleSegs.reduce((sum, s) => sum + s.value, 0)\n          : readBarValue(d, dataKey);\n      const isActive = activeIndex != null && dataIndex === activeIndex;\n      const isNegative = value < 0;\n      const label = barXAccessor(d);\n      const bandX = barScale(label) ?? 0;\n      const cx = bandX + bandWidth / 2;\n\n      // `Math.abs` so negative bars produce a positive height — direction is\n      // captured by `isNegative`. Non-negative bars are floored to\n      // `minBarHeight` so short/zero values stay visible (mirrors\n      // `<Bar minBarHeight>`); a floored bar skips the perspective trim so its\n      // tiny front face and lid stay aligned. With the default floor of 0,\n      // zero-value bars produce no height and are skipped.\n      const valuePos = yScale(value) ?? innerHeight;\n      const rawHeight = Math.abs(zeroY - valuePos);\n      const naturalHeight = isNegative\n        ? rawHeight\n        : Math.max(rawHeight, minBarHeight);\n      if (naturalHeight <= 0) {\n        return [];\n      }\n      const isFloored = !isNegative && rawHeight < naturalHeight;\n\n      const offsetFromCenter = centerX > 0 ? (cx - centerX) / centerX : 0;\n      const isRightOfCenter = offsetFromCenter > 0;\n      const absOffset = Math.min(1, Math.abs(offsetFromCenter));\n      const { depth, perspectiveRise: rawRise } = barDepthAndRise(\n        absOffset,\n        naturalHeight,\n        maxDepth\n      );\n      // Perspective lift applies only to positive bars. For negative bars a\n      // lift would raise the lid's back edge above the baseline (a visible\n      // \"lip\" at the chart edges), so it's forced to 0 — degenerating the lid\n      // and flattening the side into a rectangle flush with the baseline.\n      const perspectiveRise = isNegative ? 0 : rawRise;\n\n      // Perspective trim — positive value-bearing bars only. The lid's back\n      // edge sits `perspectiveRise` above `topY`. Without trim the silhouette\n      // top would vary with depth (bars further from center look taller).\n      // Trim shifts `topY` DOWN by `perspectiveRise` so the lid's back edge\n      // lands on `yScale(value)` regardless of depth. The clamp denominator\n      // is the topmost segment's scaled height (for stacked bars) or the whole\n      // bar's natural height, matching how `<Bar>` stacks segments.\n      let trimClampReference = naturalHeight;\n      if (visibleSegs && visibleSegs.length > 0) {\n        const segTotal = visibleSegs.reduce((sum, s) => sum + s.value, 0);\n        const topmost = visibleSegs.at(-1);\n        if (segTotal > 0 && topmost) {\n          trimClampReference = (topmost.value / segTotal) * naturalHeight;\n        }\n      }\n      const isValueBar = naturalHeight > 0 && !isNegative && !isFloored;\n      const topYTrim = isValueBar\n        ? Math.min(perspectiveRise, Math.max(0, trimClampReference - 1))\n        : 0;\n      // Direction-aware top/bottom: positive bars hang ABOVE the baseline;\n      // negative bars hang BELOW it. The lid always lives at `topY` so for\n      // negative bars it sits on the baseline.\n      const topY = isNegative ? zeroY : zeroY - naturalHeight + topYTrim;\n      const bottomY = isNegative ? zeroY + naturalHeight : zeroY;\n      const barHeight = bottomY - topY;\n\n      return [\n        {\n          label,\n          dataIndex,\n          datum: d,\n          isActive,\n          isNegative,\n          baselineY: zeroY,\n          bandX,\n          bandWidth,\n          topY,\n          bottomY,\n          barHeight,\n          naturalHeight,\n          topYTrim,\n          depth,\n          perspectiveRise,\n          isRightOfCenter,\n        },\n      ];\n    });\n  }, [\n    data,\n    barScale,\n    bandWidth,\n    yScale,\n    innerHeight,\n    innerWidth,\n    barXAccessor,\n    segmentsAccessor,\n    minBarHeight,\n    dataKey,\n    activeIndex,\n  ]);\n}\n\nfunction useBarDepthStagger() {\n  const { data, animationDuration } = useChartStable();\n  // Mirror `Bar`'s stagger formula so each depth surface enters in lockstep\n  // with its front face (`Bar` uses `animationDuration * 0.4 / count`).\n  const total = animationDuration || 1100;\n  return data.length > 1 ? (total * 0.4) / 1000 / data.length : 0;\n}\n\n// ─── Components ───────────────────────────────────────────────────────\n\nexport interface BarDepthBackProps {\n  /** Key in data for the bar value. Match the sibling `<Bar dataKey>`. */\n  dataKey: string;\n  /** Solid color for the side + top faces. Default: var(--chart-line-primary). */\n  color?: string;\n  /** Per-bar color override; takes precedence over `color` when provided. */\n  colorAccessor?: ColorAccessor;\n}\n\n/**\n * Renders the back surfaces (side + top faces) of the 3D bar effect.\n *\n * Place BEFORE the `<Bar>` elements in `<BarChart>` children so the bar's\n * solid front face occludes any depth that would extend into an adjacent\n * column. Skips bars at chart center (depth ≈ 0) and dense charts.\n *\n * For stacked bars, wrap the `<BarChart>` in a\n * `<BarDepthProvider segmentsAccessor={...}>` so the side face splits into\n * per-segment parallelograms and the lid color matches the topmost segment.\n */\nexport function BarDepthBack({\n  dataKey,\n  color = DEFAULT_BAR_COLOR,\n  colorAccessor,\n}: BarDepthBackProps) {\n  const { isLoaded, enterTransition, revealEpoch = 0 } = useChartStable();\n  const { hoveredBarIndex } = useChartHover();\n  const { segmentsAccessor } = useContext(BarDepthContext);\n  const entries = useBarDepthEntries(dataKey);\n  const stagger = useBarDepthStagger();\n  const { posStops: glassStops, negStops: glassNegStops } = useGlassStops();\n  const idPrefix = useId().replace(/[^a-z0-9]/gi, \"_\");\n\n  // Two horizontal black gradients (one per depth direction) shade the side\n  // from front-edge (lit) to back-edge (in shadow). A single vertical white\n  // gradient on the lid: bright catch at the front lip → soft white at the\n  // back. The vertical glass gradient layered on top of the side adds the same\n  // bright-tip / dark-baseline curve as the front face for continuity.\n  const sideShadeRtl = `bdb-side-rtl-${idPrefix}`;\n  const sideShadeLtr = `bdb-side-ltr-${idPrefix}`;\n  const topShade = `bdb-top-${idPrefix}`;\n  const sideGlass = `bdb-glass-${idPrefix}`;\n  // Per-bar bbox variant used only for bars that extend below the baseline.\n  const sideGlassNeg = `bdb-glass-neg-${idPrefix}`;\n\n  return (\n    <g className=\"bar-depth-back\" pointerEvents=\"none\">\n      <defs>\n        {/* Side shade — diagonal: lit corner at front-top, dark corner at\n            back-bottom. Right-of-center bars have the side on the LEFT\n            (gradient runs (1,0)→(0,1)); left-of-center bars mirror it. */}\n        <linearGradient id={sideShadeRtl} x1=\"1\" x2=\"0\" y1=\"0\" y2=\"1\">\n          <stop offset=\"0%\" stopColor=\"black\" stopOpacity=\"0.05\" />\n          <stop offset=\"100%\" stopColor=\"black\" stopOpacity=\"0.55\" />\n        </linearGradient>\n        <linearGradient id={sideShadeLtr} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n          <stop offset=\"0%\" stopColor=\"black\" stopOpacity=\"0.05\" />\n          <stop offset=\"100%\" stopColor=\"black\" stopOpacity=\"0.55\" />\n        </linearGradient>\n        {/* Lid 3D shade — mirrors the side face's directionality so the lid\n            never reads as a brighter \"white-washed cap\". Front lip at opacity\n            0 (seamless with the front face), back edge picks up shadow as it\n            angles into perspective. */}\n        <linearGradient id={topShade} x1=\"0\" x2=\"0\" y1=\"1\" y2=\"0\">\n          <stop offset=\"0%\" stopColor=\"black\" stopOpacity=\"0\" />\n          <stop offset=\"100%\" stopColor=\"black\" stopOpacity=\"0.18\" />\n        </linearGradient>\n        {/* Side glass — per-bar (objectBoundingBox): 0% = the bar's tip,\n            100% = its baseline, same proportional gloss as the front face. */}\n        <linearGradient id={sideGlass} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n          {glassStops.map((stop) => (\n            <stop\n              key={`${stop.offset}-${stop.opacity}`}\n              offset={stop.offset}\n              stopColor={stop.color}\n              stopOpacity={stop.opacity}\n            />\n          ))}\n        </linearGradient>\n        {/* Side glass (per-bar bbox variant for negative bars). The gradient\n            runs from the bar's baseline-edge (top of bbox) down to the bar's\n            value tip (bottom of bbox), lifting the tip glow onto each bar's\n            own value edge. */}\n        <linearGradient id={sideGlassNeg} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n          {glassNegStops.map((stop) => (\n            <stop\n              key={`${stop.offset}-${stop.opacity}`}\n              offset={stop.offset}\n              stopColor={stop.color}\n              stopOpacity={stop.opacity}\n            />\n          ))}\n        </linearGradient>\n      </defs>\n\n      {entries.map((e) => {\n        if (e.depth < BAR_DEPTH_MIN_PX) {\n          return null;\n        }\n\n        // Resolve stacked segments for this bar. Filter to non-zero so a\n        // missing segment doesn't paint an invisible 0-height piece or quietly\n        // become the lid color.\n        const rawSegments = segmentsAccessor?.(e.datum);\n        const visibleSegments = rawSegments?.filter((s) => s.value > 0) ?? null;\n        const baseColor = resolveColor(\n          e.datum,\n          e.dataIndex,\n          color,\n          colorAccessor\n        );\n\n        const sidePieces = buildSidePieces(e, visibleSegments, baseColor);\n        // Full-side path for the vertical glass overlay — drawn once per bar\n        // so the brightness curve sweeps across the entire side as one layer.\n        const fullSidePath = sideFacePath(e, e.topY, e.bottomY);\n        // Lid is one parallelogram regardless of segmentation — colored by the\n        // topmost visible segment so it matches the front face's top section.\n        const topPath = lidFacePath(e);\n        const topColor = visibleSegments?.at(-1)?.color ?? baseColor;\n\n        const delay = e.dataIndex * stagger;\n        // Grow uses the SAME enter transition + stagger as `<Bar>` so the side\n        // and lid rise at the exact speed of the front face. Post-load geometry\n        // changes get a quick settle instead.\n        const growTransition = isLoaded\n          ? { duration: 0.3, ease: \"easeOut\" as const }\n          : transitionWithDelay(enterTransition, delay);\n        const isFaded =\n          hoveredBarIndex != null && hoveredBarIndex !== e.dataIndex;\n        const sideShadeId = e.isRightOfCenter ? sideShadeRtl : sideShadeLtr;\n        const safeId = e.label.replace(/[^a-z0-9]/gi, \"_\");\n        const clipId = `bdb-clip-${idPrefix}-${safeId}`;\n        // Lid ENTRANCE only (hover-dim is owned by the wrapping group below, so\n        // the lid can never dim differently from the side). Positive lids\n        // translate up from the baseline riding the growing top edge; negative\n        // lids already sit at the baseline, so they fade in instead.\n        let lidEntrance: {\n          initial: false | { y?: number; opacity?: number };\n          animate: { y?: number; opacity?: number };\n          transition: Transition;\n        } = {\n          initial: false,\n          animate: { y: 0 },\n          transition: { y: growTransition },\n        };\n        if (!isLoaded && e.isNegative) {\n          lidEntrance = {\n            initial: { y: 0, opacity: 0 },\n            animate: { y: 0, opacity: 1 },\n            transition: { y: growTransition, opacity: growTransition },\n          };\n        } else if (!isLoaded) {\n          lidEntrance = {\n            initial: { y: e.barHeight },\n            animate: { y: 0 },\n            transition: { y: growTransition },\n          };\n        }\n\n        return (\n          // Single hover-dim group for the whole back surface — side AND lid\n          // share this one opacity so they always fade in lockstep (mirrors\n          // `<Bar>`, which puts grow on the inner rect and hover-dim on a\n          // wrapping group). Entrance animations live on the inner groups.\n          <motion.g\n            animate={{ opacity: isFaded ? BAR_FADED_OPACITY : 1 }}\n            initial={false}\n            key={e.label}\n            transition={BAR_HOVER_TRANSITION}\n          >\n            {/* Side face — a clip-rect that grows from the baseline reveals the\n                static side path(s) bottom-up, in lockstep with the bar's grow\n                animation. ClipPath beats CSS scaleY because motion writes the\n                SVG `transform` directly. */}\n            <defs>\n              <clipPath id={clipId}>\n                {/* Keyed on revealEpoch AND isLoaded so it (a) replays on\n                    every reveal and (b) snaps to final the instant `<Bar>`\n                    swaps to its static rect at isLoaded — so the side never\n                    trails the face past the animation cutoff. */}\n                <motion.rect\n                  animate={{\n                    y: e.topY - e.perspectiveRise,\n                    height: e.barHeight + e.perspectiveRise,\n                  }}\n                  initial={isLoaded ? false : { y: e.baselineY, height: 0 }}\n                  key={`${clipId}-${revealEpoch}-${isLoaded}`}\n                  transition={growTransition}\n                  width={e.bandWidth + 2 * e.depth + 2}\n                  x={e.bandX - e.depth - 1}\n                />\n              </clipPath>\n            </defs>\n            <g clipPath={`url(#${clipId})`}>\n              {/* Per-segment SOLID colors only. */}\n              {sidePieces.map((piece) => (\n                <path d={piece.path} fill={piece.color} key={piece.path} />\n              ))}\n              {/* Directional shade + Y-anchored glass painted ONCE over the\n                  full side so both gradients stay continuous across segment\n                  seams. Per-segment shading uses objectBoundingBox units, which\n                  would restart the lit→shadowed ramp at every stacked boundary\n                  and read as a notch/indent. The glass is the same global\n                  Y-anchored curve as the front face; negatives swap to the\n                  per-bar bbox variant so the tip catch lands on the bar's own\n                  bottom edge. */}\n              <path d={fullSidePath} fill={`url(#${sideShadeId})`} />\n              <path\n                d={fullSidePath}\n                fill={`url(#${e.isNegative ? sideGlassNeg : sideGlass})`}\n              />\n            </g>\n\n            {/* Top face — three paint passes: solid color → a flat tip-bright\n                highlight (the lid is the bar's top, so it gets the reflection\n                catch; it can't share the side's objectBoundingBox glass, which\n                would map the full ramp onto the lid's own tiny bbox) →\n                directional 3D shade darkening the back edge. */}\n            <motion.g\n              animate={lidEntrance.animate}\n              initial={lidEntrance.initial}\n              key={`lid-${safeId}-${revealEpoch}-${isLoaded}`}\n              transition={lidEntrance.transition}\n            >\n              <path d={topPath} fill={topColor} />\n              <path d={topPath} fill=\"white\" fillOpacity={GLASS_TIP_OPACITY} />\n              <path d={topPath} fill={`url(#${topShade})`} />\n            </motion.g>\n          </motion.g>\n        );\n      })}\n    </g>\n  );\n}\n\nBarDepthBack.displayName = \"BarDepthBack\";\n// Mark as a non-series layer so `BarChart` doesn't count it as a bar (it\n// carries a `dataKey` to pair with a Bar). Mirrors `ChartMarkers`'\n// `__isChartMarkers` flag — a static property is minification-safe, unlike\n// matching on `displayName`.\n(BarDepthBack as { __isBarDepthLayer?: boolean }).__isBarDepthLayer = true;\n\nexport interface BarDepthFrontProps {\n  /** Key in data for the bar value. Match the sibling `<Bar dataKey>`. */\n  dataKey: string;\n}\n\n/**\n * Renders the front surface: a glossy glass sheen over the bar's solid fill.\n * Place AFTER the `<Bar>` elements in `<BarChart>` children so the gradient\n * sits on top of the bar.\n *\n * Reads `segmentsAccessor` from the surrounding `<BarDepthProvider>` (if any)\n * via `useBarDepthEntries`, so the glass rectangle's `topY` automatically\n * matches the lid+side's trim. No prop wiring required.\n */\nexport function BarDepthFront({ dataKey }: BarDepthFrontProps) {\n  const { isLoaded, enterTransition, revealEpoch = 0 } = useChartStable();\n  const { hoveredBarIndex } = useChartHover();\n  const entries = useBarDepthEntries(dataKey);\n  const stagger = useBarDepthStagger();\n  const { posStops: glassStops, negStops: glassNegStops } = useGlassStops();\n  const idPrefix = useId().replace(/[^a-z0-9]/gi, \"_\");\n  const glassId = `bdf-glass-${idPrefix}`;\n  const glassIdNeg = `bdf-glass-neg-${idPrefix}`;\n\n  return (\n    <g className=\"bar-depth-front\" pointerEvents=\"none\">\n      <defs>\n        {/* Per-bar (objectBoundingBox) glass for POSITIVE bars: 0% = the bar's\n            tip, 100% = its baseline — same proportional gloss on every bar. */}\n        <linearGradient id={glassId} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n          {glassStops.map((stop) => (\n            <stop\n              key={`${stop.offset}-${stop.opacity}`}\n              offset={stop.offset}\n              stopColor={stop.color}\n              stopOpacity={stop.opacity}\n            />\n          ))}\n        </linearGradient>\n        {/* Per-bar bbox variant for negative bars — the bright catch always\n            sits on the value-bearing tip no matter how deep the bar reaches. */}\n        <linearGradient id={glassIdNeg} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n          {glassNegStops.map((stop) => (\n            <stop\n              key={`${stop.offset}-${stop.opacity}`}\n              offset={stop.offset}\n              stopColor={stop.color}\n              stopOpacity={stop.opacity}\n            />\n          ))}\n        </linearGradient>\n      </defs>\n\n      {entries.map((e) => {\n        const delay = e.dataIndex * stagger;\n        // Same enter transition + stagger as `<Bar>` so the glass rises in\n        // lockstep with the front face.\n        const baseTransition = isLoaded\n          ? { duration: 0.3, ease: \"easeOut\" as const }\n          : transitionWithDelay(enterTransition, delay);\n        const isFaded =\n          hoveredBarIndex != null && hoveredBarIndex !== e.dataIndex;\n\n        return (\n          <motion.rect\n            animate={{\n              y: e.topY,\n              height: e.barHeight,\n              opacity: isFaded ? BAR_FADED_OPACITY : 1,\n            }}\n            fill={`url(#${e.isNegative ? glassIdNeg : glassId})`}\n            // Anchor the grow at the baseline (not bottomY) so the glass grows\n            // outward in whichever direction the bar extends. Keyed on\n            // revealEpoch so it replays the grow on every reveal, like `<Bar>`.\n            initial={isLoaded ? false : { y: e.baselineY, height: 0 }}\n            key={`${e.label}-${revealEpoch}-${isLoaded}`}\n            transition={{ ...baseTransition, opacity: BAR_HOVER_TRANSITION }}\n            width={e.bandWidth}\n            x={e.bandX}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\nBarDepthFront.displayName = \"BarDepthFront\";\n(BarDepthFront as { __isBarDepthLayer?: boolean }).__isBarDepthLayer = true;\n\n// ─── BarPulse ─────────────────────────────────────────────────────────\n\n/**\n * Builds the SVG path tracing the silhouette of a 3D bar — the unioned\n * outline of front face + visible side parallelogram + lid parallelogram.\n * Used as the clip region for `BarPulse`'s wave so the gradient only paints\n * inside the bar's visible 3D shape. For dead-center bars (`depth <= 0`) the\n * silhouette degenerates to just the front rectangle.\n */\nfunction buildBarSilhouettePath(e: BarDepthEntry): string {\n  const {\n    bandX,\n    bandWidth,\n    topY,\n    bottomY,\n    depth,\n    perspectiveRise,\n    isRightOfCenter,\n  } = e;\n  if (depth <= 0) {\n    return [\n      `M ${bandX} ${topY}`,\n      `L ${bandX + bandWidth} ${topY}`,\n      `L ${bandX + bandWidth} ${bottomY}`,\n      `L ${bandX} ${bottomY}`,\n      \"Z\",\n    ].join(\" \");\n  }\n  if (isRightOfCenter) {\n    // Side face on LEFT — back shifted (-depth, -perspectiveRise).\n    return [\n      `M ${bandX - depth} ${topY - perspectiveRise}`,\n      `L ${bandX + bandWidth - depth} ${topY - perspectiveRise}`,\n      `L ${bandX + bandWidth} ${topY}`,\n      `L ${bandX + bandWidth} ${bottomY}`,\n      `L ${bandX} ${bottomY}`,\n      `L ${bandX - depth} ${bottomY - perspectiveRise}`,\n      \"Z\",\n    ].join(\" \");\n  }\n  // Side face on RIGHT — back shifted (+depth, -perspectiveRise).\n  return [\n    `M ${bandX} ${topY}`,\n    `L ${bandX + depth} ${topY - perspectiveRise}`,\n    `L ${bandX + bandWidth + depth} ${topY - perspectiveRise}`,\n    `L ${bandX + bandWidth + depth} ${bottomY - perspectiveRise}`,\n    `L ${bandX + bandWidth} ${bottomY}`,\n    `L ${bandX} ${bottomY}`,\n    \"Z\",\n  ].join(\" \");\n}\n\nexport interface BarPulseProps {\n  /** Key in data for the bar value. Match the sibling `<Bar dataKey>`. */\n  dataKey: string;\n  /** Index (in the data array) of the bar to pulse. */\n  activeIndex?: number;\n  /** Suppress the sweep while keeping the bar's 3D + glass treatment. */\n  pulsePaused?: boolean;\n}\n\n/**\n * Vertical wave that sweeps from a bar's root (baseline) toward its tip on a\n * continuous loop, painted across the bar's full 3D silhouette (front face +\n * lid + visible side). Reads as a heartbeat pulse flowing up the column for\n * positive bars and down for negative bars — useful for marking a live or\n * in-progress value without adding any fixed element.\n *\n * Select the bar via `activeIndex`. Set `pulsePaused` to freeze it. Place\n * AFTER `<BarDepthFront />` so the wave brightens on top of the glass.\n *\n * Performance: at most one clipped `<motion.rect>` with a single Y translate\n * per frame (no per-frame paint of gradient stops, no SVG filters).\n */\nexport function BarPulse({ dataKey, activeIndex, pulsePaused }: BarPulseProps) {\n  const { isLoaded } = useChartStable();\n  const { hoveredBarIndex } = useChartHover();\n  const entries = useBarDepthEntries(dataKey, activeIndex);\n  const idPrefix = useId().replace(/[^a-z0-9]/gi, \"_\");\n  const activeEntries = useMemo(\n    () => entries.filter((e) => e.isActive),\n    [entries]\n  );\n\n  if (activeEntries.length === 0 || pulsePaused) {\n    return null;\n  }\n  // Hold the wave until bars finish growing — the silhouette clip is computed\n  // at the final geometry, so painting it mid-grow would reveal a wave inside\n  // an area the bar hasn't reached yet.\n  if (!isLoaded) {\n    return null;\n  }\n\n  return (\n    <g className=\"bar-pulse\" pointerEvents=\"none\">\n      <defs>\n        {activeEntries.flatMap((e) => {\n          const safeId = e.label.replace(/[^a-z0-9]/gi, \"_\");\n          const clipId = `bar-pulse-clip-${idPrefix}-${safeId}`;\n          const gradId = `bar-pulse-grad-${idPrefix}-${safeId}`;\n          return [\n            <clipPath id={clipId} key={`clip-${safeId}`}>\n              <path d={buildBarSilhouettePath(e)} />\n            </clipPath>,\n            // Vertical gradient with a centered bright band shaped into a soft\n            // bell curve. y1=1, y2=0 makes offset 50% the rect's center, so the\n            // brightest pixel sits mid-rect during travel.\n            <linearGradient\n              id={gradId}\n              key={`grad-${safeId}`}\n              x1=\"0\"\n              x2=\"0\"\n              y1=\"1\"\n              y2=\"0\"\n            >\n              <stop offset=\"0%\" stopColor=\"white\" stopOpacity=\"0\" />\n              <stop offset=\"10%\" stopColor=\"white\" stopOpacity=\"0\" />\n              <stop\n                offset=\"22%\"\n                stopColor=\"white\"\n                stopOpacity={PULSE_WAVE_PEAK_OPACITY * 0.18}\n              />\n              <stop\n                offset=\"34%\"\n                stopColor=\"white\"\n                stopOpacity={PULSE_WAVE_PEAK_OPACITY * 0.5}\n              />\n              <stop\n                offset=\"44%\"\n                stopColor=\"white\"\n                stopOpacity={PULSE_WAVE_PEAK_OPACITY * 0.85}\n              />\n              <stop\n                offset=\"50%\"\n                stopColor=\"white\"\n                stopOpacity={PULSE_WAVE_PEAK_OPACITY}\n              />\n              <stop\n                offset=\"56%\"\n                stopColor=\"white\"\n                stopOpacity={PULSE_WAVE_PEAK_OPACITY * 0.85}\n              />\n              <stop\n                offset=\"66%\"\n                stopColor=\"white\"\n                stopOpacity={PULSE_WAVE_PEAK_OPACITY * 0.5}\n              />\n              <stop\n                offset=\"78%\"\n                stopColor=\"white\"\n                stopOpacity={PULSE_WAVE_PEAK_OPACITY * 0.18}\n              />\n              <stop offset=\"90%\" stopColor=\"white\" stopOpacity=\"0\" />\n              <stop offset=\"100%\" stopColor=\"white\" stopOpacity=\"0\" />\n            </linearGradient>,\n          ];\n        })}\n      </defs>\n\n      {activeEntries.map((e) => {\n        const safeId = e.label.replace(/[^a-z0-9]/gi, \"_\");\n        const clipId = `bar-pulse-clip-${idPrefix}-${safeId}`;\n        const gradId = `bar-pulse-grad-${idPrefix}-${safeId}`;\n\n        const waveHeight = Math.max(\n          e.barHeight * PULSE_WAVE_HEIGHT_RATIO,\n          PULSE_WAVE_HEIGHT_MIN_PX\n        );\n        // Travel always flows root → tip. Positive bars sweep up (root =\n        // bottomY); negative bars sweep down (root = topY, the baseline). The\n        // gradient is symmetric, so reversing the endpoints reverses direction.\n        const yAboveLid = e.topY - e.perspectiveRise - waveHeight;\n        const yBelowFloor = e.bottomY;\n        const yStart = e.isNegative ? yAboveLid : yBelowFloor;\n        const yEnd = e.isNegative ? yBelowFloor : yAboveLid;\n\n        const isFaded =\n          hoveredBarIndex != null && hoveredBarIndex !== e.dataIndex;\n\n        return (\n          <motion.g\n            animate={{ opacity: isFaded ? BAR_FADED_OPACITY : 1 }}\n            clipPath={`url(#${clipId})`}\n            initial={false}\n            key={e.label}\n            transition={BAR_HOVER_TRANSITION}\n          >\n            <motion.rect\n              animate={{ y: yEnd }}\n              fill={`url(#${gradId})`}\n              height={waveHeight}\n              initial={{ y: yStart }}\n              transition={{\n                duration: PULSE_WAVE_DURATION_S,\n                ease: \"easeInOut\",\n                repeat: Number.POSITIVE_INFINITY,\n                repeatType: \"loop\",\n              }}\n              // Span wide enough to cover the bar plus its depth on either\n              // side; the clipPath crops anything outside the silhouette.\n              width={e.bandWidth + 2 * e.depth + 2}\n              x={e.bandX - e.depth - 1}\n            />\n          </motion.g>\n        );\n      })}\n    </g>\n  );\n}\n\nBarPulse.displayName = \"BarPulse\";\n(BarPulse as { __isBarDepthLayer?: boolean }).__isBarDepthLayer = true;\n",
      "type": "registry:component",
      "target": "components/charts/bar-depth.tsx"
    }
  ]
}